Introduction
Abstract classes define a contract that subclasses must fulfill while also providing shared implementation. Mixins extend this idea by composing behaviors from multiple sources into a single class — something TypeScript supports through a pattern of class-returning functions.
Key Concepts
- Abstract Class: A class that cannot be instantiated directly. It may contain both abstract methods (no body, must be overridden) and concrete methods (with implementation).
- Mixin: A function that takes a base class constructor and returns a new class extending it with additional functionality.
- Constructor Type: The
new (...args: any[]) => Tsignature used to type class constructors in mixin functions.
Real World Context
Frameworks like Angular and NestJS use abstract classes for base controllers and services. Mixins are used in libraries like Lit (web components) and TypeORM to compose behaviors such as timestamping, soft-deleting, and auditing without deep inheritance hierarchies.
Deep Dive
Abstract Classes
typescriptabstract class Shape { abstract area(): number; abstract perimeter(): number; // Concrete method — shared by all subclasses describe(): string { return `Area: ${this.area().toFixed(2)}, Perimeter: ${this.perimeter().toFixed(2)}`; } } class Circle extends Shape { constructor(private radius: number) { super(); } area(): number { return Math.PI * this.radius ** 2; } perimeter(): number { return 2 * Math.PI * this.radius; } } class Rectangle extends Shape { constructor(private width: number, private height: number) { super(); } area(): number { return this.width * this.height; } perimeter(): number { return 2 * (this.width + this.height); } } // const s = new Shape(); // Error: Cannot create an instance of an abstract class const c = new Circle(5); console.log(c.describe()); // "Area: 78.54, Perimeter: 31.42"
Abstract classes combine the interface contract with shared implementation that subclasses inherit for free.
Mixins
TypeScript models mixins as functions that accept a class constructor and return a new class:
typescripttype Constructor<T = {}> = new (...args: any[]) => T; function Timestamped<TBase extends Constructor>(Base: TBase) { return class extends Base { createdAt = new Date(); updatedAt = new Date(); touch() { this.updatedAt = new Date(); } }; } function SoftDeletable<TBase extends Constructor>(Base: TBase) { return class extends Base { deletedAt: Date | null = null; isDeleted = false; softDelete() { this.deletedAt = new Date(); this.isDeleted = true; } }; } // Compose mixins class BaseEntity { id = crypto.randomUUID(); } const TimestampedEntity = Timestamped(BaseEntity); const FullEntity = SoftDeletable(TimestampedEntity); const entity = new FullEntity(); entity.touch(); // from Timestamped entity.softDelete(); // from SoftDeletable console.log(entity.id); // from BaseEntity
Each mixin adds one behavior without creating a deep inheritance chain.
Common Pitfalls
- Diamond problem — If two mixins add a method with the same name, the last one applied wins. Be deliberate about composition order.
- Constructor signatures — Mixins that add constructor parameters are more complex. Prefer mixins that use property initializers over constructor arguments.
Best Practices
- Keep mixins focused — Each mixin should add exactly one concern (timestamps, soft-delete, auditing). This keeps composition simple and predictable.
- Use abstract classes for "is-a" relationships — If the hierarchy models a taxonomy (Shape → Circle), use abstract classes. If you are adding cross-cutting concerns, use mixins.
Summary
- Abstract classes combine interface contracts with shared implementation.
- Mixins compose behaviors by chaining class-returning functions.
- Use abstract classes for taxonomies and mixins for cross-cutting concerns.
- Be mindful of method name collisions in mixin composition order.
Code Examples
type Constructor<T = {}> = new (...args: any[]) => T;
// Mixin that adds serialization
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this);
}
};
}
class User {
constructor(public name: string, public email: string) {}
}
const SerializableUser = Serializable(User);
const user = new SerializableUser("Alice", "alice@example.com");
console.log(user.serialize());
// '{"name":"Alice","email":"alice@example.com"}'