TypeScript Mixin
A Mixin is a code reuse pattern used to mix multiple independent functional modules into a single class.
TypeScript implements Mixins by using generic functions to return extended classes, compensating for the limitation that single inheritance cannot reuse behavior from multiple sources.
SVG Diagram: Mixin Composition ProcessBasic Concepts
The core of a Mixin is a generic function that takes a base class and returns an extended class.ConstructorThe type constraint is used to describe "any class that can be inherited."
Example
type Constructor<T = {}> = new (...args: any[]) => T;
// Mixin function: takes a base class and returns a new extended class
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}
// Base class
class User {
constructor(public name: string) {}
}
// Get a new class after mixing in
const TimestampedUser = Timestamped(User);
const user = new TimestampedUser("Alice");
console.log(user.name); // Alice
console.log(user.createdAt instanceof Date); // true
Output:
Alice true
Note:A Mixin does not modify the original class; it returns a brand new extended class. The original
Userclass is not affected.
Combining Multiple Mixins
Nesting multiple Mixin functions lets you stack multiple capabilities onto the same class in sequence.
Example
// Mixin 1: Add timestamp
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}
// Mixin 2: Add serialization
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this);
}
};
}
// Mixin 3: Add logging (depends on the serialize method)
function Loggable<TBase extends Constructor<{ serialize(): string }>>(Base: TBase) {
return class extends Base {
log(): void {
console.log("[LOG]", this.serialize());
}
};
}
class Product {
constructor(public name: string, public price: number) {}
}
// Stack three Mixins in sequence
const AdvancedProduct = Loggable(Serializable(Timestamped(Product)));
const p = new AdvancedProduct("Phone", 999);
p.log();
console.log(p.createdAt instanceof Date); // true
Output:
[LOG] {"name":"Phone","price":999,"createdAt":"2026-..."}
true
Mixin and Interfaces
The class returned by a Mixin can declare that it implements an interface, allowing consumers to operate on the mixed object through the interface type without depending on the concrete implementation.
Example
// Define a capability interface
interface ISerializable {
serialize(): string;
}
interface ICloneable<T> {
clone(): T;
}
// Mixin implements the interface
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base implements ISerializable {
serialize(): string {
return JSON.stringify(this);
}
};
}
function Cloneable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
clone() {
return Object.assign(
Object.create(Object.getPrototypeOf(this)),
this
);
}
};
}
class Article {
constructor(public title: string, public content: string) {}
}
const RichArticle = Cloneable(Serializable(Article));
const a1 = new RichArticle("TypeScript Introduction", "Content...");
const a2 = a1.clone();
a2.title = "TypeScript Advanced";
console.log(a1.serialize());
// {"title":"TypeScript Introduction","content":"Content..."}
console.log(a2.serialize());
// {"title":"TypeScript Advanced","content":"Content..."}
console.log(a1.title === a2.title); // false (independently modified after cloning)
Output:
{"title":"TypeScript 入门","content":"正文内容..."}
{"title":"TypeScript 进阶","content":"正文内容..."}
false
Constrained Mixin
Using generic constraints, you can restrict a Mixin to only mix into classes that meet specific conditions, avoiding missing required properties at runtime.
Example
// Constraint: the base class must have id and name properties
type WithIdAndName = Constructor<{ id: number; name: string }>;
function Printable<TBase extends WithIdAndName>(Base: TBase) {
return class extends Base {
print(): void {
console.log(`[${this.id}] ${this.name}`);
}
};
}
class Item {
constructor(public id: number, public name: string) {}
}
// Correct: Item satisfies the constraint
const PrintableItem = Printable(Item);
const item = new PrintableItem(42, "Keyboard");
item.print(); // [42] Keyboard
// Error example (the compiler will prevent it):
// class NoId { constructor(public name: string) {} }
// const Bad = Printable(NoId); // Error: NoId lacks the id property
Output:
[42] Keyboard
Mixin vs Inheritance
| Dimension | Inheritance (extends) | Mixin |
|---|---|---|
| Number of sources | Can only inherit one parent class | Can stack any number |
| Coupling degree | Strong coupling between subclass and parent class | Each Mixin is independent, low coupling |
| Reuse granularity | Reuse the capabilities of the entire class | Reuse a single feature on demand |
| Type safety | Native support | Needs to be ensured via generic constraints |
| Applicable scenarios | Strong "is-a" relationship | Cross-cutting concerns (logging, serialization, caching, etc.) |
Summary
- Core pattern:A Mixin is a generic function that takes a base class and returns an extended class,
Constructor<T>is the standard constraint type - Capability stacking:Nesting multiple Mixin functions allows combining multiple capabilities into the same class
- Interface integration:The class returned by a Mixin can implement interfaces; consumers only need to depend on interfaces rather than concrete classes
- Generic constraints:Through constraints,
TBaseyou can limit the applicable scope of a Mixin and prevent incorrect usage at compile time - Applicable scenarios:Logging, serialization, cloning, timestamps and other cross-cutting concerns; better than multi-level inheritance