TypeScript 81 🔷 Mixins and Class Composition
Inheritance is the first tool most developers reach for when they need to share behavior between classes. But single inheritance has a hard limit: a class can extend only one parent. When you need to combine behavior from multiple sources — a class that can log, serialize, and render, but each of those capabilities lives in a separate class — inheritance forces you to pick one lineage and duplicate the rest. Mixins solve this problem by composing classes from smaller, orthogonal pieces .
TypeScript has supported the mixin class pattern since version 2.2, with specific rules for how mixin constructors interact with regular constructors in intersection types . The pattern uses generics and class expressions to extend a base class at runtime while preserving full type information. This chapter covers the class expression pattern, constrained mixins, the alternative interface-merging approach, and the real-world trade-offs between mixins, composition, and inheritance.
Key point: A TypeScript mixin is a function that takes a base class and returns a new class extending it. The base class is passed as a generic parameter constrained to a constructor type. The returned class adds properties and methods, and TypeScript represents the result as an intersection between the mixin class and the parametric base class. This gives you multiple inheritance-like composition without the diamond problem .
Why mixins exist
Class hierarchies in real applications rarely fit a clean single-inheritance tree. Consider a sprite in a game engine. It has a position (x, y), a name, and a render method. Now you want some sprites to be scalable, some to be jumpable, some to be loggable. If you model each capability as a base class, you end up with ScalableSprite extends Sprite, JumpableSprite extends Sprite, and ScalableJumpableSprite extends ???. There is no single parent that gives you both Scale and Jumpable.
The orthogonal features problem. Features like logging, timestamping, serialization, and validation cut across inheritance hierarchies. They are not variations of a type; they are capabilities that any type might need. Mixins let you attach those capabilities without forcing a hierarchy .
The composition problem. Traditional composition — holding an instance of another class as a property — works well when the relationship is “has a.” But sometimes you want the composed behavior to feel like it belongs to the object directly. sprite.setScale(0.8) reads better than sprite.scaler.setScale(0.8). Mixins merge behavior into the prototype chain, so the methods are called directly on the instance .
The boilerplate problem. Without mixins, sharing method implementations between unrelated classes means copy-pasting code or writing helper functions that take an object and mutate it. Mixins encode the sharing in the type system, so the compiler understands what the composed class can do .
The trade-off. Mixins are powerful but can obscure where a method comes from. When you call sprite.jump(), the compiler knows it exists, but a reader looking at Sprite won’t see it defined. Mixins also introduce the risk of property name collisions — if two mixins define id, one overwrites the other . Use mixins for genuinely orthogonal features, not as a replacement for clear inheritance or composition.
a. The Class Expression Pattern
The recommended mixin pattern in TypeScript uses a function that takes a base class and returns a new class extending it. The base class is typed as a generic constrained to a constructor type .
// A type representing any class constructor
type Constructor = new (...args: any[]) => {};
// The base class
class Sprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
// A mixin that adds scale functionality
function Scale<TBase extends Constructor>(Base: TBase) {
return class Scaling extends Base {
// Mixins may not declare private/protected properties
// Use ES2020 private fields instead
#scale = 1;
setScale(scale: number) {
this.#scale = scale;
}
get scale(): number {
return this.#scale;
}
};
}
// Compose the base class with the mixin
const EightBitSprite = Scale(Sprite);
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log(flappySprite.scale); // 0.8
The Constructor type declares that the argument is a class constructor — something callable with new that returns an object. The generic TBase extends Constructor ensures the mixin only accepts valid class constructors. The returned class expression extends Base and adds the Scale functionality. When you call Scale(Sprite), TypeScript computes the result as an intersection: typeof Scaling & typeof Sprite. The instance type becomes Scaling & Sprite, which includes both the name, x, y properties from Sprite and the setScale, scale from the mixin .
One important rule: mixins may not declare private or protected properties. The TypeScript compiler cannot statically verify that a mixin applied to one class won’t conflict with private members of another. Use ES2020 private fields (#field) instead, which are enforced at runtime and avoid the static conflict .
The Constructor type is flexible but unconstrained. It accepts any class. If a mixin depends on properties or methods that the base class must provide, you need a constrained mixin.
b. Constrained Mixins
A constrained mixin restricts which base classes it can be applied to. Instead of accepting any Constructor, it accepts a constructor whose instances have a specific shape .
// A generic constructor type that preserves the instance type
type GConstructor<T = {}> = new (...args: any[]) => T;
// Constraints: base class must have setPos method
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
// Mixin that only works on Positionable classes
function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
jump() {
// This only compiles because Positionable guarantees setPos
this.setPos(0, 20);
}
};
}
// Usage: base class must satisfy Positionable
class SpriteWithPos {
x = 0;
y = 0;
setPos(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const JumpingSprite = Jumpable(SpriteWithPos);
const sprite = new JumpingSprite();
sprite.jump(); // works — setPos is available
The GConstructor<T> type differs from the plain Constructor by preserving the instance type T. This lets the constraint express “a constructor that returns an object with a setPos method.” If you try to apply Jumpable to a class that lacks setPos, TypeScript rejects it at compile time.
This constraint mechanism is how mixins document their requirements. A mixin that calls this.someMethod() must constrain its base to a type that has someMethod. Without the constraint, the mixin would compile, but applying it to an incompatible class would fail at runtime .
c. The Interface-Merging Alternative
Before TypeScript 2.2, the recommended mixin pattern used interface merging and a runtime applyMixins helper. This pattern is still valid and is sometimes preferred when the mixins are simple and you want to avoid class expressions .
// Each capability is a plain class
class Jumpable {
jump() {
console.log("Jumping!");
}
}
class Duckable {
duck() {
console.log("Ducking!");
}
}
// The base class
class Sprite {
x = 0;
y = 0;
}
// Interface merging: Sprite now also has jump and duck
interface Sprite extends Jumpable, Duckable {}
// Apply the mixins at runtime
applyMixins(Sprite, [Jumpable, Duckable]);
// Helper that copies prototype properties
function applyMixins(derivedCtor: any, constructors: any[]) {
constructors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
Object.create(null)
);
});
});
}
const sprite = new Sprite();
sprite.jump(); // works
sprite.duck(); // works
The interface Sprite extends Jumpable, Duckable tells the compiler that Sprite instances have the methods from both mixin classes. The applyMixins function copies the prototype methods at runtime. This pattern relies less on the compiler and more on runtime behavior — the compiler trusts the interface declaration, and the helper ensures the methods exist.
The class expression pattern is generally preferred because it keeps the runtime and type hierarchies in sync automatically. The interface-merging pattern requires you to declare the merged interface manually, and forgetting that declaration means the methods exist at runtime but not in the type system .
A practical limitation of the class expression pattern with class expressions (as opposed to class declarations) is that you cannot merge an interface with a class variable. If const User = class { ... }, you cannot write interface User extends Mixin {}. The workaround is to use a type alias or a temporary variable with the correct type .
Complete Example Session
This session builds a logging mixin, a timestamp mixin, and a serialization mixin, composing them onto a base class and demonstrating the type inference.
// ============================================
// PART 1: THE CONSTRUCTOR TYPES
// ============================================
// Unconstrained constructor
type Constructor = new (...args: any[]) => {};
// Generic constructor preserving instance type
type GConstructor<T = {}> = new (...args: any[]) => T;
// ============================================
// PART 2: THE BASE CLASS
// ============================================
class Entity {
id: string;
createdAt: Date;
constructor(id: string) {
this.id = id;
this.createdAt = new Date();
}
}
// ============================================
// PART 3: THE LOGGING MIXIN
// ============================================
function WithLogging<TBase extends Constructor>(Base: TBase) {
return class Logging extends Base {
log(message: string) {
console.log(`[${this.constructor.name}] ${message}`);
}
};
}
// ============================================
// PART 4: THE TIMESTAMP MIXIN
// ============================================
function WithTimestamp<TBase extends Constructor>(Base: TBase) {
return class Timestamp extends Base {
#updatedAt: Date | null = null;
touch() {
this.#updatedAt = new Date();
}
get updatedAt(): Date | null {
return this.#updatedAt;
}
};
}
// ============================================
// PART 5: THE SERIALIZATION MIXIN (CONSTRAINED)
// ============================================
interface Identifiable {
id: string;
}
type IdentifiableCtor = GConstructor<Identifiable>;
function WithSerialization<TBase extends IdentifiableCtor>(Base: TBase) {
return class Serializable extends Base {
serialize(): string {
return JSON.stringify({ id: this.id });
}
};
}
// ============================================
// PART 6: COMPOSING THE MIXINS
// ============================================
const LoggableEntity = WithLogging(Entity);
const TimestampedLoggableEntity = WithTimestamp(LoggableEntity);
const FullEntity = WithSerialization(TimestampedLoggableEntity);
// ============================================
// PART 7: USING THE COMPOSED CLASS
// ============================================
const user = new FullEntity("user-1");
user.log("User created"); // [Logging] User created
user.touch();
console.log(user.updatedAt); // current date
console.log(user.serialize()); // {"id":"user-1"}
// ============================================
// PART 8: THE PROPERTY COLLISION TRAP
// ============================================
// ❌ Two mixins both defining `id`
function MixinA<TBase extends Constructor>(Base: TBase) {
return class extends Base {
id = "from-A";
};
}
function MixinB<TBase extends Constructor>(Base: TBase) {
return class extends Base {
id = "from-B";
};
}
const Collision = MixinA(MixinB(Entity));
const collision = new Collision("original");
// collision.id is "from-A" — MixinA overrides MixinB
// No compiler error. This is a runtime hazard.
// ============================================
// PART 9: THE CLASS EXPRESSION LIMITATION
// ============================================
// ❌ Cannot merge interface with class expression variable
// const User = class { ... };
// interface User extends Mixin {} // Error
// ✅ Use type alias instead
const User = class {
name = "default";
};
type User = typeof User & { extraMethod: () => void };
// ============================================
// PART 10: THE DECLARATION MERGING WORKAROUND
// ============================================
// For class declarations, interface merging works:
class Person {
name = "";
}
interface Person extends WithLogging<typeof Person> {}
// But the runtime mixin must still be applied manually.
// The interface declaration alone does not modify the class.
The ten parts cover constructor types, the base class, a logging mixin, a timestamp mixin, a constrained serialization mixin, composition chaining, usage, the property collision trap, the class expression limitation, and the declaration merging workaround.
Quick Reference
The Mixin Pattern
| Step | Code |
|---|---|
| Define constructor type | type Constructor = new (...args: any[]) => {}; |
| Define generic constructor | type GConstructor<T> = new (...args: any[]) => T; |
| Write mixin function | function Mixin<TBase extends Constructor>(Base: TBase) { ... } |
| Apply mixin | const Mixed = Mixin(BaseClass); |
| Create instance | new Mixed(...args) |
The Mixin Rules
| Rule | Reason |
|---|---|
| Constrain base to constructor type | Ensures Base can be extended |
Use ES2020 #private fields | Mixins cannot declare private/protected |
| Spread constructor args | constructor(...args: any[]) { super(...args); } |
| Result is intersection type | typeof Mixin & typeof Base |
The Comparison
| Approach | Type Safety | Runtime | When to Use |
|---|---|---|---|
| Class expression | Automatic | Prototype chain | Recommended default |
| Interface merging | Manual | applyMixins | Simple mixins, legacy code |
| Composition | Full | Explicit delegation | “Has a” relationships |
| Inheritance | Full | Prototype chain | “Is a” relationships |
Best Practices
✅ Do This:
// Constrain the base class when the mixin needs specific members
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
function Jumpable<TBase extends Positionable>(Base: TBase) { ... } // ✅
// Use ES2020 private fields inside mixins
function Scale<TBase extends Constructor>(Base: TBase) {
return class extends Base {
#scale = 1; // ✅
};
}
// Keep each mixin focused on one capability
function WithLogging<TBase extends Constructor>(Base: TBase) { ... } // ✅
// Document the base class requirements
// This mixin requires a class with `id: string`
function WithSerialization<TBase extends IdentifiableCtor>(Base: TBase) { ... } // ✅
❌ Don’t Do This:
// Don't declare private/protected in mixins
function Bad<TBase extends Constructor>(Base: TBase) {
return class extends Base {
private secret = "x"; // ❌ TypeScript error
};
}
// Don't use mixins for "is a" relationships
function AnimalMixin<TBase extends Constructor>(Base: TBase) { ... } // ❌ use inheritance
// Don't ignore property collisions
// Two mixins defining `id` silently overwrite // ❌
// Don't overuse mixins
// Every class does not need to be a mixin composition // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
private in mixin fails | TypeScript forbids it | Use #field instead |
| Mixin methods not recognized | Class expression + interface merge fails | Use type alias type User = typeof User & Mixin |
| Property name collision | Two mixins define same property | Use unique naming or namespace |
| Base class constraint not enforced | Using unconstrained Constructor | Use GConstructor<T> with required shape |
| Runtime methods missing | Interface merged but applyMixins not called | Always call the runtime helper |
| Mixin chain loses type info | Applying mixins in wrong order | Composition order is left-to-right |
Real-World Examples
1. Logging Mixin
function WithLogging<TBase extends Constructor>(Base: TBase) {
return class extends Base {
log(msg: string) { console.log(`[${this.constructor.name}] ${msg}`); }
};
}
2. Timestamp Mixin
function WithTimestamp<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}
3. Serialization Mixin (Constrained)
type IdentifiableCtor = GConstructor<{ id: string }>;
function WithSerialization<TBase extends IdentifiableCtor>(Base: TBase) {
return class extends Base {
serialize() { return JSON.stringify({ id: this.id }); }
};
}
4. Validation Mixin
function WithValidation<TBase extends Constructor>(Base: TBase) {
return class extends Base {
validate(): boolean { return true; }
};
}
5. Persistence Mixin
function WithPersistence<TBase extends Constructor>(Base: TBase) {
return class extends Base {
save() { localStorage.setItem(this.constructor.name, JSON.stringify(this)); }
};
}
6. Event Emitter Mixin
function WithEvents<TBase extends Constructor>(Base: TBase) {
return class extends Base {
#listeners = new Map<string, Function[]>();
on(event: string, fn: Function) { /* ... */ }
emit(event: string, data: unknown) { /* ... */ }
};
}
7. Compose Two Mixins
const LoggableTimestamped = WithLogging(WithTimestamp(Entity));
8. Apply Mixin to Multiple Bases
const LoggableSprite = WithLogging(Sprite);
const LoggableEntity = WithLogging(Entity);
9. Constrain to Interface
interface Renderable { render(): void; }
function WithTooltip<TBase extends GConstructor<Renderable>>(Base: TBase) { ... }
10. Declaration Merging Workaround
const User = class { name = ""; };
type User = typeof User & { greet(): void; };
Visual: Mixin Composition
┌──────────────────────────────────────────────┐
│ MIXIN COMPOSITION │
│ │
│ Sprite (base) │
│ ├─ name │
│ ├─ x │
│ └─ y │
│ │ │
│ ▼ │
│ Scale(Sprite) │
│ └─ setScale(), scale │
│ │ │
│ ▼ │
│ Jumpable(Scale(Sprite)) │
│ └─ jump() │
│ │
│ Result: Sprite + Scale + Jumpable │
│ One class, three capabilities. │
│ │
└──────────────────────────────────────────────┘
Visual: Class Expression Pattern
┌──────────────────────────────────────────────┐
│ CLASS EXPRESSION PATTERN │
│ │
│ type Constructor = new(...args) => {} │
│ │ │
│ ▼ │
│ function Mixin<T extends Constructor> │
│ (Base: T) { │
│ return class extends Base { │
│ newMethod() { ... } │
│ }; │
│ } │
│ │ │
│ ▼ │
│ const Mixed = Mixin(BaseClass); │
│ │ │
│ ▼ │
│ new Mixed() → instance has │
│ BaseClass + Mixin methods │
│ │
└──────────────────────────────────────────────┘
Visual: Constrained Mixin
┌──────────────────────────────────────────────┐
│ CONSTRAINED MIXIN │
│ │
│ type Positionable = GConstructor<{ │
│ setPos: (x, y) => void │
│ }>; │
│ │
│ function Jumpable<T extends Positionable> │
│ (Base: T) { │
│ return class extends Base { │
│ jump() { │
│ this.setPos(0, 20); │
│ // ✅ setPos exists because of T │
│ } │
│ }; │
│ } │
│ │
│ Jumpable(SpriteWithPos) ✅ │
│ Jumpable(Entity) ❌ no setPos │
│ │
└──────────────────────────────────────────────┘
Visual: Mixin vs Intersection vs Composition
┌──────────────────────────────────────────────┐
│ MIXIN vs INTERSECTION vs COMPOSITION │
│ │
│ Mixin: │
│ ├─ Modifies prototype │
│ ├─ Affects all instances │
│ └─ Methods feel native │
│ │
│ Intersection: │
│ ├─ Creates new object │
│ ├─ Does not modify original │
│ └─ Type-only (no prototype) │
│ │
│ Composition: │
│ ├─ Holds instance as property │
│ ├─ Explicit delegation │
│ └─ "Has a" relationship │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Mixin definition | Function taking base class, returning extended class |
| Constructor type | new (...args: any[]) => {} |
| Generic constructor | GConstructor<T> = new (...args: any[]) => T |
| Constraint | <TBase extends GConstructor<Required>> |
| Private fields | Use #field, not private |
| Result type | typeof Mixin & typeof Base |
| Interface merging | Manual applyMixins helper |
| Class expression limit | Cannot merge interface with variable |
Key takeaways:
- Mixins compose classes from orthogonal pieces. A function takes a base class and returns a new class extending it with additional behavior. This provides multiple inheritance-like composition without the hierarchy constraints .
- The class expression pattern is the recommended approach. It keeps the runtime and type hierarchies in sync automatically. The result is an intersection type:
typeof Mixin & typeof Base. - Constrain mixins to require specific base class members. Use
GConstructor<T>whereTdescribes the required shape. The compiler then enforces that only compatible classes are passed . - Mixins cannot declare
privateorprotectedproperties. The compiler cannot verify compatibility across applications. Use ES2020#privatefields instead . - Interface merging is the alternative pattern. Define mixin classes separately, merge interfaces with
interface Base extends Mixin1, Mixin2, and callapplyMixinsat runtime. This relies more on manual synchronization . - Property name collisions are a runtime hazard. Two mixins defining the same property silently overwrite each other. There is no compile-time error. Use unique naming or namespace your capabilities .
- Mixins are not always the right tool. For “is a” relationships, use inheritance. For “has a” relationships, use composition. Mixins work best for genuinely orthogonal, reusable capabilities .
Remember: Mixins are a composition pattern, not an inheritance replacement. They let you attach capabilities — logging, serialization, validation — to classes without forcing a hierarchy. The class expression pattern is the modern, type-safe approach. Constrain your mixins, avoid property collisions, and remember that composition and inheritance still have their place. Use mixins when the feature is orthogonal and reusable; use other tools when the relationship is structural.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!