The short answer
Name a constructor type that accepts any arguments, make the mixin generic over it with @template, and return a class that extends the parameter:
/** @typedef {new (...args: any[]) => object} Constructor */
/**
* @template {Constructor} TBase
* @param {TBase} Base
*/
export function Timestamped(Base) {
return class extends Base {
createdAt = new Date();
updatedAt = new Date();
touch() {
this.updatedAt = new Date();
}
};
}
class Order {
/** @param {string} id */
constructor(id) {
this.id = id;
}
}
export const TimestampedOrder = Timestamped(Order);
const order = new TimestampedOrder("A-1001");
order.touch();
console.log(order.id, order.createdAt.toISOString());type Constructor = new (...args: any[]) => object;
export function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
updatedAt = new Date();
touch() {
this.updatedAt = new Date();
}
};
}
class Order {
id: string;
constructor(id: string) {
this.id = id;
}
}
export const TimestampedOrder = Timestamped(Order);
const order = new TimestampedOrder("A-1001");
order.touch();
console.log(order.id, order.createdAt.toISOString());jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} TimestampedOrder takes the same constructor arguments as Order, and its instances have id, createdAt, updatedAt and touch().
Why it works
new (...args: any[]) => object is the type of “any class”. With @template {Constructor} TBase, the parameter Base keeps the exact class you pass, and class extends Base produces a class type that combines Base’s instance type with the new members. TypeScript has supported this pattern since 2.2; the handbook’s mixins page uses the same shape.
The same thing in TypeScript
The TypeScript equivalent tab writes function Timestamped<TBase extends Constructor>(Base: TBase). The body, a class expression extending the parameter, is plain JavaScript and the same in both files.
What goes wrong
The original class does not gain the new members: only the class the mixin returns has them (TS2339). And the mixed class keeps the base constructor’s parameter types (TS2345):
/** @typedef {new (...args: any[]) => object} Constructor */
/**
* @template {Constructor} TBase
* @param {TBase} Base
*/
export function Timestamped(Base) {
return class extends Base {
createdAt = new Date();
};
}
class Order {
/** @param {string} id */
constructor(id) {
this.id = id;
}
}
const TimestampedOrder = Timestamped(Order);
const plain = new Order("A-1001");
console.log(plain.createdAt);error TS2339: Property 'createdAt' does not exist on type 'Order'.
new TimestampedOrder(1001);error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} The constraint must accept any constructor arguments. A narrower constructor type, here one that takes an id, is refused at the extends (TS2545):
/**
* @template {new (id: string) => object} TBase
* @param {TBase} Base
*/
export function Timestamped(Base) {
return class extends Base {error TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'.
createdAt = new Date();
};
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Gotchas
- Use
object, not{}, for the instance type. Both work as the constraint;objectsays “an object” without allowing primitives. - Name the mixed class.
export const TimestampedOrder = Timestamped(Order)gives the result a name to import; to use it as a type in JSDoc, writeInstanceType<typeof TimestampedOrder>. @mixinis not read. JSDoc’s@mixinand@mixestags are documentation for jsdoc.app; TypeScript types mixins only through the functions above.
Questions
Can I apply two mixins to one class?
Yes, by nesting the calls: Timestamped(SoftDeletable(Order)). Each mixin's result is a class, so it can be the base for the next one, and the final class has all their members.
Can the mixin require members on the base class?
Yes. Constrain the instance type in the constructor type: @template {new (...args: any[]) => { id: string }} TBase lets the mixin's methods use this.id, and refuses base classes without an id.
Sources
- Mixins · TypeScript handbook
- TypeScript 2.2 release notes (mixin classes) · TypeScript handbook
Last reviewed 21 October 2026.