Mixins & Composition
The problem with deep inheritance
Section titled “The problem with deep inheritance”Single inheritance forces every capability into one rigid chain. If Timestamped, Serializable, and Comparable are all behaviors you want to share across unrelated classes, a linear hierarchy cannot express that — you would have to pick one parent and duplicate the rest.
Mixins solve this by making a behavior a function that adds features to any base class, so a class can compose several.
A mixin is a class factory
Section titled “A mixin is a class factory”A mixin is a function that takes a base class (a constructor) and returns a new class that extends it with extra behavior.
type Constructor = new (...args: any[]) => {};
function Timestamped<TBase extends Constructor>(Base: TBase) { return class extends Base { createdAt = new Date(); };}
function Activatable<TBase extends Constructor>(Base: TBase) { return class extends Base { isActive = false; activate() { this.isActive = true; } };}
class User { name: string; constructor(name: string) { this.name = name; }}
// Compose several behaviors onto User:const SmartUser = Activatable(Timestamped(User));
const u = new SmartUser("Ada");u.createdAt; // from Timestampedu.activate(); // from Activatableu.name; // from UserThe generic TBase extends Constructor constraint is what makes this type-safe: it says “whatever class you pass in, I return a class that has all of its members plus the new ones.” TypeScript tracks the accumulated shape through each wrap.
flowchart LR base["class User"] --> m1["Timestamped(...) + createdAt"] m1 --> m2["Activatable(...) + activate()"] m2 --> result["SmartUser has all three"]
Constrained mixins
Section titled “Constrained mixins”Sometimes a mixin needs the base class to already have certain members. Constrain the base to a constructor that produces the required shape:
type Positioned = new (...args: any[]) => { x: number; y: number };
function Movable<TBase extends Positioned>(Base: TBase) { return class extends Base { move(dx: number, dy: number) { this.x += dx; // ✅ allowed — the constraint guarantees x exists this.y += dy; } };}Now Movable can only be applied to a base whose instances have x and y, and inside it those members are known to exist.
Composition over inheritance
Section titled “Composition over inheritance”Mixins are one form of a broader principle: prefer assembling behavior from small, independent pieces over deriving it through a rigid hierarchy. Often you do not even need mixins — plain object composition (holding collaborators as fields and delegating to them) is simpler still and avoids the class-factory machinery entirely. Reach for mixins when you genuinely need the composed result to be a class with all behaviors merged onto its instances.
Naming the mixin backbone: Constructor<T>, typeof C, and InstanceType
Section titled “Naming the mixin backbone: Constructor<T>, typeof C, and InstanceType”Every mixin in this lesson relies on the same shape — new (...args: any[]) => T — which is worth naming and understanding on its own, separate from the mixin pattern itself. Any class name, used as a value rather than a type annotation, has the type typeof ClassName: the type of the constructor, not the type of an instance.
class User { name: string; constructor(name: string) { this.name = name; }}
type UserCtor = typeof User; // the CONSTRUCTOR type -- new (name: string) => Usertype UserInstance = InstanceType<UserCtor>; // "unwrap" a constructor type back to its instance type
function makeOne(Ctor: UserCtor, name: string): InstanceType<UserCtor> { return new Ctor(name);}
const u: UserInstance = makeOne(User, "Ada");console.log(u.name);InstanceType<C> is the utility type that does the reverse of typeof: given a constructor type, it extracts the type of what new-ing it produces. This pair — typeof ClassName to talk about the constructor, InstanceType<...> to get back to the instance — is exactly what every mixin factory in this lesson is built from: type Constructor<T = {}> = new (...args: any[]) => T is a generic, unnamed constructor type, matching any class whose instances are (at least) shaped like T. Naming it once, as a reusable helper, is the idiomatic version of the inline new (...args: any[]) => {} this lesson opened with:
type Constructor<T = {}> = new (...args: any[]) => T;
function Loggable<TBase extends Constructor>(Base: TBase) { return class extends Base { log() { console.log("logging"); } };}TBase extends Constructor is the mixin backbone in full: it constrains TBase to “some constructor,” lets the mixin extends Base (which requires a real constructor type, not just an object shape), and — combined with Constructor<SomeRequiredShape> as in Movable above — lets a mixin additionally demand that the base’s instances already have specific members.
Mixins and private don’t compose — and the reason is structural, not incidental
Section titled “Mixins and private don’t compose — and the reason is structural, not incidental”The official TypeScript handbook states the limitation plainly: “Mixins may not declare private/protected properties, however, you can use ES2020 private fields.” A #private field declared inside a mixin’s returned class body works exactly as you’d expect — it’s real ECMAScript syntax, scoped to that one class expression, with nothing generic about it:
type Constructor<T = {}> = new (...args: any[]) => T;
function Loggable<TBase extends Constructor>(Base: TBase) { return class extends Base { #callCount = 0; // a #private field declared inside a mixin -- this is fine log() { this.#callCount++; console.log(`logged ${this.#callCount} times`); } };}
class Account {}const LoggableAccount = Loggable(Account);const a = new LoggableAccount();a.log();What genuinely doesn’t work is trying to constrain a mixin’s base to already have a private (or protected) member — because a constrained mixin’s constraint is an ordinary object type, and object type literals cannot express access modifiers at all:
// @expect-error TS1070type Constructor<T = {}> = new (...args: any[]) => T;type WithSecret = Constructor<{ private secret: string }>;error TS1070: 'private' modifier cannot appear on a type member.This is a syntax error, not a design choice you could work around with different typing — interface and object-type-literal members simply have no concept of visibility; every member of an object type is implicitly public. So “mixins and private don’t compose” means precisely this: you can freely use #private inside a mixin’s own returned class, but you can never require, via a Constructor<...> constraint, that a mixin’s base class already provides a private/protected member — there is no type-level vocabulary to say so.
Abstract mixin classes and the abstract constructor type
Section titled “Abstract mixin classes and the abstract constructor type”A mixin that never actually calls new Base() itself — because the mixin only ever adds methods on top, and construction is deferred to whatever eventually extends the result — can accept an abstract class as its base, but only if the mixin’s type signature says so explicitly. Since TypeScript 4.2, abstract new (...args: any[]) => T (an abstract constructor type) describes exactly this: a constructor that can be extended but not called with a bare new.
type Constructor<T = {}> = new (...args: any[]) => T;type AbstractConstructor<T = {}> = abstract new (...args: any[]) => T;
abstract class Shape { abstract area(): number;}
// This mixin's base only needs to be extendable, never directly constructed --// so it accepts an ABSTRACT constructor, and can take an abstract class as input.function Describable<TBase extends AbstractConstructor<Shape>>(Base: TBase) { abstract class Described extends Base { describe() { return `area is ${this.area()}`; } } return Described;}
const DescribableShape = Describable(Shape); // ✅ Shape is abstract -- fine here
class Square extends DescribableShape { side: number; constructor(side: number) { super(); this.side = side; } area() { return this.side * this.side; }}
console.log(new Square(3).describe());Using the ordinary (non-abstract) Constructor<Shape> instead rejects Shape outright, with two separate, precise errors:
// @expect-error TS2653 TS2345type Constructor<T = {}> = new (...args: any[]) => T;
abstract class Shape { abstract area(): number;}
function Describable<TBase extends Constructor<Shape>>(Base: TBase) { return class extends Base { describe() { return "area"; } };}
const DescribableShape = Describable(Shape);error TS2653: Non-abstract class expression does not implement inherited abstract member 'area' from class 'Shape'.error TS2345: Argument of type 'typeof Shape' is not assignable to parameter of type 'Constructor<Shape>'. Cannot assign an abstract constructor type to a non-abstract constructor type.The first error is the mixin’s own returned class expression getting caught by the same abstract-member check any concrete subclass of Shape would face — a non-abstract class extending Shape must implement area(), and class extends Base { describe() {...} } doesn’t. The second names the deeper issue directly: Shape’s constructor type is abstract (it cannot be new-ed directly), and Constructor<Shape> demands a non-abstract, directly-callable constructor — an abstract constructor type is never assignable to a plain one, precisely because the plain one promises “you can call new on this,” which an abstract class cannot honor.
An abstract-constructor-typed mixin accepting an abstract base
type AbstractConstructor<T = {}> = abstract new (...args: any[]) => T;
abstract class Shape { abstract area(): number;}
function Describable<TBase extends AbstractConstructor<Shape>>(Base: TBase) { abstract class Described extends Base { describe() { return `area is ${this.area()}`; } } return Described;}
const DescribableShape = Describable(Shape);
class Square extends DescribableShape { side: number; constructor(side: number) { super(); this.side = side; } area() { return this.side * this.side; }}
console.log(new Square(3).describe()); Click Check types: zero diagnostics. Click Run: area is 9. This is the general shape behind any mixin library built to work with both concrete and abstract base classes — accept an abstract constructor type when the mixin itself never instantiates its base directly.
Mental Model
Section titled “Mental Model”Every mixin signature answers two questions about its TBase type parameter: what must the base class’s constructor look like (plain new (...args: any[]) => T, or abstract new (...args: any[]) => T if the mixin never calls new Base() itself), and what must an already-constructed instance already provide (the T in Constructor<T>, which becomes whatever shape the mixin’s own methods rely on, like Movable’s { x: number; y: number }). Read any mixin by answering those two questions before reading its body — the constraint tells you exactly what bases it accepts and why, without needing to trace through the returned class expression at all.
Trade-offs
Section titled “Trade-offs”| Approach | Benefit | Cost |
|---|---|---|
Mixin (TBase extends Constructor) | Composes several independent behaviors onto one class; the compiler tracks the merged shape through each wrap | class-factory machinery; #private works inside, but bases can’t be required to have private members |
| Plain object composition | Simpler, no generic constructor gymnastics at all | The composed result isn’t one merged class — collaborators stay separate objects you delegate to explicitly |
| Abstract-constructor-typed mixin | Accepts both concrete and abstract bases, since it never calls new Base() itself | Requires knowing the abstract new (...args: any[]) => T syntax exists at all — an easy detail to miss |
Ordinary Constructor<T>-typed mixin | Simpler signature | Silently rejects any abstract base with two compounding errors (TS2653, TS2345), which can look unrelated to the real cause at first glance |
Common Misconceptions
Section titled “Common Misconceptions”- A mixin can require its base class to already have a
privatemember, the same wayMovablerequiresx/y. It cannot — a mixin’s constraint is an ordinary object type, and object types have no syntax forprivate/protectedat all (TS1070). Only public shape can be required this way. #privatefields “don’t work” with mixins, full stop. They work fine inside a mixin’s own returned class body — real ECMAScript private fields are unaffected by the generic machinery around them. What doesn’t work is requiring one via a constraint.- Any mixin that accepts a
Constructor<Shape>should also acceptabstract class Shape. It won’t —Constructor<T>demands a directly-callable, non-abstract constructor. A mixin has to opt in explicitly withabstract new (...args: any[]) => Tto accept abstract bases. typeof SomeClassandSomeClass(as a type annotation) mean the same thing. They don’t:SomeClassas a type is the instance type (whatx: SomeClassrequires);typeof SomeClassis the constructor type (what you’d type a variable holding the class itself, to benew-ed later).
Reading AI-generated code
Section titled “Reading AI-generated code”Generated mixin code tends to reach for the loosest possible constraint that happens to compile, which erases exactly the safety a well-typed mixin should provide:
- A mixin constrained with a bare
Constructor(no type argument, so it defaults toConstructor<{}>) that then accesses specific members onthis(this.x,this.save()) without those members appearing anywhere in the constraint is relying onany-like leniency, not a real guarantee — check whether the constraint actually names every member the mixin body uses, the wayMovable’sPositionedconstraint namesxandy. - A mixin typed with the ordinary
Constructor<T>applied to a library’sabstractbase class will fail with the TS2653/TS2345 pair shown above. If the generated code’s fix is to stripabstractfrom the base class instead of widening the mixin’s constraint toAbstractConstructor<T>, that fix throws away a real design constraint (the base was abstract on purpose) to route around a typing gap that has a proper, narrow solution.
🐛 Spot the bug
type Constructor<T = {}> = new (...args: any[]) => T;
function Serializable<TBase extends Constructor>(Base: TBase) {return class extends Base { serialize() { return JSON.stringify(this.toPayload()); }};}
class Order {id = "ord_1";toPayload() { return { id: this.id };}}
const SerializableOrder = Serializable(Order);const o = new SerializableOrder();console.log(o.serialize());Show the answer
This fails to compile: TS2339: Property 'toPayload' does not exist on type '(Anonymous class)'. Serializable is constrained with a bare Constructor — which is Constructor<{}>, since the type parameter defaults to {} — so inside the mixin body, this is only known to have whatever the anonymous class’s own declared members provide, plus whatever {} guarantees from the base: nothing beyond that. Calling this.toPayload() assumes every base passed to Serializable has a toPayload method, but nothing in the constraint says so, so the compiler correctly refuses to assume it. The fix mirrors Movable’s pattern from earlier in this lesson: constrain the base explicitly, type Payloadable = Constructor<{ toPayload(): unknown }>, then write function Serializable<TBase extends Payloadable>(Base: TBase). Now the constraint documents exactly what Serializable requires, and this.toPayload() type-checks because every valid TBase is guaranteed to provide it.