TypeScript Mixins
Mixins are TypeScript's answer to a limit JavaScript inherited from its class syntax: a class can extend exactly one other class. The moment two unrelated features need the same three methods, you either copy them, invent an awkward base class, or reach for mixins — functions that take a class and hand back an extended version of it.
The pattern is older than TypeScript and works fine in plain JavaScript. What TypeScript adds is a way to type it, so the composed class still knows every member it picked up along the way. That typing is where most of the friction lives, and it is most of what this article is about.
The problem mixins solve
Say two records both need a creation timestamp and a way to refresh it.
class ArticleDoc {
createdAt = new Date()
touch(): void {
this.createdAt = new Date()
}
}
class CommentDoc {
createdAt = new Date()
touch(): void {
this.createdAt = new Date()
}
}The duplication is small here, but it compounds. A shared TimestampedBase class fixes it right
up until the day CommentDoc also needs the serialisation helpers that live on a different base.
Single inheritance has no answer for that. Mixins do, because a mixin is a function, and functions
compose.
What a mixin is
A mixin is a function that takes a class, declares a subclass of it, and returns that subclass.
To type the parameter, you need a type that means "something you can call new on":
[object Object]That is a function type with a new signature. The
...args: any[] rest parameter is not laziness — TypeScript requires it, and we will come back to
why. With it in hand, the mixin itself is six lines:
class UserRecord {
constructor(public name: string) {}
}
function WithTimestamp<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date()
}
}
const TimestampedUser = WithTimestamp(UserRecord)
const firstUser = new TimestampedUser('ada')
console.log(firstUser.name, firstUser.createdAt)WithTimestamp is generic over the base class, so the return type is not
some flattened object — it is "whatever you passed in, plus createdAt". firstUser.name still
type-checks, because UserRecord went in and never got lost.
Note the class expression has no name. It does not need one, and leaving it anonymous avoids shadowing the function in stack traces you will eventually read.
Composing several mixins
Because each mixin takes a class and returns a class, they nest:
function WithSerial<TBase extends Constructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this)
}
}
}
const TrackedUser = WithSerial(WithTimestamp(UserRecord))
const secondUser = new TrackedUser('grace')
console.log(secondUser.serialize(), secondUser.createdAt, secondUser.name)Reading order matters: the innermost call is applied first, so WithSerial sees a class that
already has createdAt. Three mixins deep the expression gets hard to read, which is the usual
signal to stop at two or three.
Order also decides who wins a collision. If two mixins both define serialize, the outer one
overrides the inner, exactly as it would with ordinary subclassing — and nothing warns you,
because overriding a method is perfectly legal. That silence is the strongest argument for keeping
the number of mixins on any one class small and their member names specific.
The resulting type is an intersection
This is the part worth internalising. The instance type of a mixed-in class is the base type and each mixin's additions — an intersection type, built for you by the compiler:
type TrackedUserInstance = InstanceType<typeof TrackedUser>
declare const tracked: TrackedUserInstance
const trackedName: string = tracked.name
const trackedAt: Date = tracked.createdAt
const trackedJson: string = tracked.serialize()All three assignments check. Nothing was widened to any, and no interface had to be written by
hand. If you ever need to name that type in a signature, InstanceType<typeof X> is the way to
get at it — the class expression is anonymous, so there is no other handle.
Constrained mixins
A mixin often needs the base class to already have something. Put the requirement in the constraint:
type Named = { name: string }
function WithGreeting<TBase extends Constructor<Named>>(Base: TBase) {
return class extends Base {
greet(): string {
return `Hello, ${this.name}`
}
}
}
const GreetingUser = WithGreeting(UserRecord)
console.log(new GreetingUser('linus').greet())
class Anonymous {}
// const broken = WithGreeting(Anonymous)
// ❌ Property 'name' is missing in type 'Anonymous' but required in type 'Named'Constructor<Named> reads as "a constructor that produces something with a name". UserRecord
qualifies, Anonymous does not, and the error lands at the call site rather than inside the mixin
body. That is the whole benefit of constraining instead of casting.
Two pitfalls worth knowing
A mixin cannot see what comes after it
Each mixin is typed against its own base, not against the finished class. A mixin applied first has no idea what a later one will add:
function WithLogging<TBase extends Constructor>(Base: TBase) {
return class extends Base {
log(): void {
// this.serialize()
// ❌ Property 'serialize' does not exist on type 'WithLogging<TBase>'
console.log('logged')
}
}
}
const LoggedUser = WithLogging(WithSerial(UserRecord))
new LoggedUser('rich').log()If log genuinely needs serialize, express it as a constraint the way WithGreeting does, and
apply the mixins in the order that satisfies it.
Mixin constructors take ...args: any[] and nothing else
A mixin class may not declare its own typed constructor parameters:
function WithVersion<TBase extends Constructor>(Base: TBase) {
return class extends Base {
version = 1
constructor(...args: any[]) {
super(...args)
this.version = 1
}
}
}
// return class extends Base {
// constructor(public version: number) { super() }
// }
// ❌ ts(2545): A mixin class must have a constructor with a single rest
// parameter of type 'any[]'.The compiler needs a single, predictable constructor shape to thread the base's arguments through. Anything a mixin needs to configure should therefore be a field, a setter, or a parameter of the mixin function itself rather than of the class it returns.
Configurable mixins
That last point has a useful escape hatch. Since a mixin is just a function, it can take its own arguments and close over them — the configuration lives in the factory, not in the constructor:
function withPrefix(prefix: string) {
return function <TBase extends Constructor<Named>>(Base: TBase) {
return class extends Base {
get displayName(): string {
return `${prefix}${this.name}`
}
}
}
}
const AdminUser = withPrefix('admin:')(UserRecord)
const admin = new AdminUser('ada')
console.log(admin.displayName)Two calls instead of one, which reads oddly the first time. The payoff is that prefix is fixed
when the class is created rather than passed to every instance, and the closure keeps it genuinely
private — no field, no #name, nothing on the prototype to inspect.
Mixing into abstract classes
A plain Constructor will not accept an abstract class, because you cannot call new on one.
TypeScript has a separate signature for that case:
type AbstractConstructor<T = {}> = abstract new (...args: any[]) => T
abstract class ShapeNode {
abstract area(): number
}
function WithLabel<TBase extends AbstractConstructor>(Base: TBase) {
abstract class Labelled extends Base {
label = 'shape'
}
return Labelled
}
const LabelledShape = WithLabel(ShapeNode)
class CircleNode extends LabelledShape {
constructor(private radius: number) {
super()
}
area(): number {
return Math.PI * this.radius ** 2
}
}
const circle = new CircleNode(2)
console.log(circle.label, circle.area())The returned class stays abstract, so the obligation to implement area passes down to
CircleNode where it belongs.
Mixins versus decorators
Both bolt behaviour onto a class, and they are not interchangeable.
Decorators run at definition time and are good at observing or
replacing what is already there — logging a method, registering a class, wrapping a getter. What
they cannot do is add members that the type system then knows about: decorate a class with
something that attaches createdAt at runtime, and the type still has no createdAt.
Mixins work the other way round. They are plain values flowing through plain functions, so the type follows the value. If you need new members to be visible to the compiler, that is a mixin. If you need to intercept existing ones, that is a decorator.
There is a practical difference too. Decorators are an evolving part of the language with two
incompatible generations — the legacy experimentalDecorators flavour and the stage-3 standard
one — so a decorator-heavy design ties itself to a particular compiler configuration. Mixins are
functions and classes, nothing more. They have behaved the same way for years and need no flags
at all, which makes them the safer default in code other people compile.
Declaration merging is the third option when a mixin has to attach itself to a named class rather
than a returned one — an interface with the same name as the class adds members to its type.
See interfaces for how that merging works.
When not to use a mixin
Most of the time. A mixin earns its complexity when the behaviour genuinely needs the instance's own state and has to be available on many unrelated classes. Short of that, injection is simpler and far easier to test:
type Clock = { now(): Date }
const systemClock: Clock = { now: () => new Date() }
class InvoiceRecord {
constructor(private clock: Clock) {}
stamp(): Date {
return this.clock.now()
}
}
console.log(new InvoiceRecord(systemClock).stamp())No generics, no constructor-type gymnastics, and the clock is swappable in a test. The honest rule of thumb: reach for a mixin when you have three or more classes that need the same stateful behaviour and no sensible shared parent. With two, copy the code.
Once mixins feel comfortable, the ClassPublicKeys challenge is a good follow-up — pulling a class's public property names out at the type level is exactly the skill you need to debug a composed class whose type is not what you expected. For the type-level side of the same idea, see the TypeScript class guide.
Summary
A mixin is a generic function that takes a constructor and returns a subclass of it. Constructor
(or AbstractConstructor) types the parameter, the rest parameter has to be ...args: any[], and
the instance type that comes out is an intersection of everything applied. Constrain the base when
a mixin depends on it, remember that each mixin only sees what came before it, and prefer plain
composition until three classes actually need the same behaviour.
Become a TypeScript Pro
Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges