Skip to main content

Mapped Types

Overview

A MappedType derives a new type from an existing ComplexType by transforming its fields — picking a subset, omitting unwanted ones, or changing their optionality — without redeclaring any fields manually.

OPRA provides four factory functions that mirror TypeScript's built-in utility types:

FactoryTypeScript equivalentWhat it does
PickType(Base, keys)Pick<T, K>Keeps only the listed fields
OmitType(Base, keys)Omit<T, K>Removes the listed fields
PartialType(Base, keys)Partial<T>Makes the specified fields optional
RequiredType(Base, keys)Required<T>Makes the specified fields required

All four return a class that can be used anywhere a ComplexType is accepted — as a base class, as a field type, or as a standalone registered type.


PickType

PickType creates a new type containing only the specified fields from the base type.

import { ComplexType, ApiField, PickType } from '@opra/common';

@ComplexType()
class Customer {
@ApiField({ required: true }) declare givenName: string;
@ApiField({ required: true }) declare familyName: string;
@ApiField() declare email?: string;
@ApiField() declare address?: Address;
@ApiField() declare internalScore?: number;
}

// Only givenName and familyName are kept
const CustomerSummary = PickType(Customer, ['givenName', 'familyName']);

Register it as a named type by wrapping it with @ComplexType():

@ComplexType({ name: 'CustomerSummary' })
class CustomerSummary extends PickType(Customer, ['givenName', 'familyName']) {}

All field constraints (required, readonly, deprecated, etc.) are preserved from the base type for the picked fields.


OmitType

OmitType creates a new type containing all fields from the base type except the listed ones.

import { ComplexType, ApiField, OmitType } from '@opra/common';

@ComplexType()
class Customer {
@ApiField({ required: true }) declare givenName: string;
@ApiField({ required: true }) declare familyName: string;
@ApiField() declare email?: string;
@ApiField() declare internalScore?: number;
@ApiField() declare passwordHash?: string;
}

// Drop internal fields before exposing to the public API
const PublicCustomer = OmitType(Customer, ['internalScore', 'passwordHash']);

As a registered named type:

@ComplexType({ name: 'PublicCustomer' })
class PublicCustomer extends OmitType(Customer, ['internalScore', 'passwordHash']) {}

PartialType

PartialType makes the specified fields optional by setting required: false on them. The second argument — the list of field names — is required.

import { PartialType } from '@opra/common';

@ComplexType()
class CreateCustomerDto {
@ApiField({ required: true }) declare givenName: string;
@ApiField({ required: true }) declare familyName: string;
@ApiField({ required: true }) declare email: string;
}

@ComplexType({ name: 'UpdateCustomerDto' })
class UpdateCustomerDto extends PartialType(CreateCustomerDto, ['email']) {
// givenName and familyName remain required
// email becomes optional
}

RequiredType

RequiredType forces the specified fields to be required by setting required: true on them. The second argument — the list of field names — is required.

import { RequiredType } from '@opra/common';

@ComplexType()
class CustomerDraft {
@ApiField() declare givenName?: string;
@ApiField() declare familyName?: string;
@ApiField() declare email?: string;
}

@ComplexType({ name: 'CustomerCreateDto' })
class CustomerCreateDto extends RequiredType(CustomerDraft, ['givenName', 'familyName']) {
// givenName and familyName become required
// email stays optional
}

Combining Transformations

Factory functions can be chained — the output of one becomes the input of the next.

import { PickType, PartialType, RequiredType } from '@opra/common';

@ComplexType()
class Customer {
@ApiField({ required: true }) declare _id: number;
@ApiField({ required: true }) declare givenName: string;
@ApiField({ required: true }) declare familyName: string;
@ApiField() declare email?: string;
@ApiField() declare phone?: string;
@ApiField() declare internalScore?: number;
}

// 1. Pick only the fields we want
// 2. Make editable fields optional (for a PATCH body)
// 3. Force _id to remain required (it's the identifier)
@ComplexType({ name: 'PatchCustomerDto' })
class PatchCustomerDto extends RequiredType(
PartialType(
PickType(Customer, ['_id', 'givenName', 'familyName', 'email']),
['givenName', 'familyName', 'email'],
),
['_id'],
) {}

// Result: { _id: required, givenName?: optional, familyName?: optional, email?: optional }

Using MappedType as a Base

A MappedType can be used as the base for a @ComplexType(), allowing you to add new fields on top of the transformation.

import { OmitType, ComplexType, ApiField } from '@opra/common';

@ComplexType()
class User {
@ApiField({ required: true }) declare _id: number;
@ApiField({ required: true }) declare email: string;
@ApiField() declare passwordHash?: string;
@ApiField() declare createdAt?: Date;
}

// Remove sensitive field, then add new fields
@ComplexType({ name: 'UserProfile' })
class UserProfile extends OmitType(User, ['passwordHash']) {
@ApiField() declare avatarUrl?: string;
@ApiField() declare bio?: string;
}

// UserProfile fields: _id, email, createdAt, avatarUrl, bio

MappedTypes themselves can also be chained as bases:

const Mapped1 = PickType(Customer, ['_id', 'givenName', 'familyName', 'email', 'phone']);
const Mapped2 = OmitType(Mapped1, ['phone']);

@ComplexType({ name: 'CustomerCard' })
class CustomerCard extends Mapped2 {
@ApiField() declare displayName?: string;
}

Using MappedType as a Field Type

Pass a MappedType factory result directly to @ApiField({ type: ... }) for inline, anonymous type projections.

import { PickType, OmitType } from '@opra/common';

@ComplexType()
class Order {
@ApiField({ required: true })
declare _id: number;

// Inline pick — only expose id and name from Product
@ApiField({ type: PickType(Product, ['_id', 'name']) })
declare product?: { _id: number; name: string };

// Inline omit — hide passwordHash from embedded User
@ApiField({ type: OmitType(User, ['passwordHash']) })
declare createdBy?: Omit<User, 'passwordHash'>;
}

This is useful for embedding a projected shape of another type without registering a separate named type.


Options Reference

All four factory functions accept an optional DataType.Options object as their last argument:

PickType(Base, keys, options?)
OmitType(Base, keys, options?)
PartialType(Base, keys, options?)
RequiredType(Base, keys, options?)
OptionTypeDescription
namestringRegistry name for the resulting type. Defaults to {BaseName}Mapped.
descriptionstringHuman-readable description included in the schema export.
abstractbooleanCannot be used directly in fields — only extended.
scopePatternstring | RegExp | (string | RegExp)[]Restricts which scopes can use this type.
embeddedbooleanNot exposed as a standalone named type in the schema.
examplesDataTypeExample[]Example values for schema documentation.
// Named and described
const CustomerSummary = PickType(Customer, ['givenName', 'familyName'], {
name: 'CustomerSummary',
description: 'Lightweight customer projection for list responses',
});

// Embedded — used inline, not exposed in the schema registry
const InlineDto = OmitType(User, ['passwordHash'], { embedded: true });