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:
| Factory | TypeScript equivalent | What 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?)
| Option | Type | Description |
|---|---|---|
name | string | Registry name for the resulting type. Defaults to {BaseName}Mapped. |
description | string | Human-readable description included in the schema export. |
abstract | boolean | Cannot be used directly in fields — only extended. |
scopePattern | string | RegExp | (string | RegExp)[] | Restricts which scopes can use this type. |
embedded | boolean | Not exposed as a standalone named type in the schema. |
examples | DataTypeExample[] | 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 });