Mixin Types
Overview
A MixinType composes fields from multiple base types into a single class — the equivalent of multiple inheritance for OPRA types. Where a regular extends chain is limited to one parent, MixinType merges any number of ComplexTypes, MappedTypes, or other MixinTypes.
ComplexTypeBase
├── ComplexType → single-parent class
├── MappedType → projected/transformed class
└── MixinType → multi-parent composite class
MixinType only accepts types from the ComplexTypeBase hierarchy. Passing a SimpleType or EnumType is not supported and will throw at registration time.
Basic Usage
Call MixinType with an array of base types to produce a composite class. Use it directly as the base of a @ComplexType():
import { ComplexType, ApiField, MixinType } from '@opra/common';
@ComplexType()
class Timestamped {
@ApiField({ readonly: true }) declare createdAt?: Date;
@ApiField({ readonly: true }) declare updatedAt?: Date;
}
@ComplexType()
class SoftDeletable {
@ApiField({ readonly: true }) declare deletedAt?: Date;
@ApiField() declare isDeleted?: boolean;
}
@ComplexType({ name: 'Article' })
class Article extends MixinType([Timestamped, SoftDeletable]) {
@ApiField({ required: true }) declare title: string;
@ApiField() declare body?: string;
}
// Article fields: createdAt, updatedAt, deletedAt, isDeleted, title, body
All fields from every listed type are merged into the resulting class. Fields declared directly on Article are added on top.
Field Merging
Fields are merged in array order. When two base types declare a field with the same name, the later type wins (last-write-wins).
import { ComplexType, ApiField, MixinType } from '@opra/common';
@ComplexType()
class Base {
@ApiField({ required: true }) declare status: string;
@ApiField() declare createdAt?: Date;
}
@ComplexType()
class Override {
// Redefines status as optional and deprecated
@ApiField({ deprecated: 'Use state instead' }) declare status?: string;
}
@ComplexType({ name: 'Entity' })
class Entity extends MixinType([Base, Override]) {}
// Entity.status: optional, deprecated (Override's version wins)
// Entity.createdAt: optional (from Base, unaffected)
To control which definition takes precedence, put the type whose version you want to keep last in the array.
Additional Fields Resolution
When base types have different additionalFields settings, MixinType resolves them with the following priority:
| Priority | Value | Behaviour |
|---|---|---|
| 1 (highest) | true | Any additional property is allowed; wins over everything |
| 2 | First defined value | The first non-undefined additionalFields among the bases |
| — | undefined / omitted | Additional properties are stripped (default) |
If any base allows all additional fields (additionalFields: true), the mixin is fully open — regardless of what the other bases say.
import { ComplexType, MixinType } from '@opra/common';
import { StringType } from '@opra/common';
@ComplexType({ additionalFields: new StringType() })
class StringMap {}
@ComplexType({ additionalFields: true })
class OpenBag {}
@ComplexType({ additionalFields: ['error'] })
class StrictDto {}
// MixinType([StringMap, StrictDto]) → additionalFields: StringType (first defined wins)
// MixinType([StrictDto, StringMap]) → additionalFields: ['error'] (first defined wins)
// MixinType([StrictDto, OpenBag]) → additionalFields: true (true always wins)
Nested MixinTypes
A MixinType can itself appear inside another MixinType array, enabling deep composition:
import { ComplexType, ApiField, MixinType } from '@opra/common';
@ComplexType()
class HasId {
@ApiField({ readonly: true }) declare _id?: number;
}
@ComplexType()
class HasTimestamps {
@ApiField({ readonly: true }) declare createdAt?: Date;
@ApiField({ readonly: true }) declare updatedAt?: Date;
}
@ComplexType()
class HasOwner {
@ApiField() declare ownerId?: number;
}
// First compose audit fields
const Auditable = MixinType([HasId, HasTimestamps]);
// Then compose that with ownership
@ComplexType({ name: 'OwnedDocument' })
class OwnedDocument extends MixinType([Auditable, HasOwner]) {
@ApiField({ required: true }) declare name: string;
}
// OwnedDocument fields: _id, createdAt, updatedAt, ownerId, name
Fields are flattened and deduplicated depth-first — nesting does not create nested objects.
Extending a MixinType
Add new fields on top of a mixin by declaring them directly on the @ComplexType() class:
import { ComplexType, ApiField, MixinType } from '@opra/common';
@ComplexType()
class Localized {
@ApiField() declare locale?: string;
@ApiField() declare timezone?: string;
}
@ComplexType()
class Contactable {
@ApiField() declare email?: string;
@ApiField() declare phone?: string;
}
@ComplexType({ name: 'UserProfile' })
class UserProfile extends MixinType([Localized, Contactable]) {
@ApiField({ required: true }) declare username: string;
@ApiField() declare avatarUrl?: string;
@ApiField() declare bio?: string;
}
// UserProfile fields: locale, timezone, email, phone, username, avatarUrl, bio
You can also chain mixins — use a MixinType as a base for another MixinType:
const BaseRecord = MixinType([HasId, HasTimestamps]);
@ComplexType({ name: 'Product' })
class Product extends MixinType([BaseRecord, Localized]) {
@ApiField({ required: true }) declare sku: string;
@ApiField() declare price?: number;
}
Options Reference
All DataType.Options are accepted as a second argument to MixinType:
MixinType(bases, options?)
| Option | Type | Description |
|---|---|---|
name | string | Registry name for the resulting type. Defaults to {BaseName}Mixin. |
description | string | Human-readable description included in the schema export. |
abstract | boolean | Cannot be used directly in fields — only extended. |
additionalFields | boolean | DataType | ['error'] | ['error', string] | Overrides the resolved additionalFields policy. |
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. |
// Standalone named mixin — registered and reusable
const AuditFields = MixinType([HasId, HasTimestamps], {
name: 'AuditFields',
description: 'Standard audit trail fields shared across all entities',
abstract: true,
});
// Anonymous embedded mixin — not exposed in the schema registry
const InlineBase = MixinType([Localized, Contactable], { embedded: true });