TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
203: Alternative Patterns to Mixins
I've seen a lot of developers gravitate toward mixins in TypeScript because they're trying to solve a classic problem: they want a class to have "multiple behaviors" without the nightmare of deep inheritance hierarchies. The problem is that TypeScript's implementation of mixins requires some pretty aggressive type gymnastics that usually end up making the code harder to read and maintain than the problem they were meant to solve.
Take a look at this. We're building a simple game engine, and we want some objects to be Movable and some to be Destructible. A player is both; a wall is only destructible.
// The "Clever" Mixin Approach
type Constructor<T> = new (...args: any[]) => T;
function Movable<TBase extends Constructor<any>>(Base: TBase) {
return class extends Base {
position = { x: 0, y: 0 };
move(dx: number, dy: number) {
this.position.x += dx;
this.position.y += dy;
}
};
}
function Destructible<TBase extends Constructor<any>>(Base: TBase) {
return class extends Base {
hp = 100;
damage(amount: number) {
this.hp -= amount;
}
};
}
class GameObject {
name: string;
constructor(name: string) { this.name = name; }
}
// This looks okay until you try to actually use it in a complex system
const Player = Movable(Destructible(GameObject));
const hero = new Player("Hero");
// Bug: If I want a function that accepts anything 'Movable',
// typing it becomes a nightmare because 'Player' is a dynamic class
function teleport(entity: any) { // I've defaulted to 'any' because the type is an anonymous class
entity.move(10, 10);
}
The Type Gymnastics Trap
The code above "works," but it's a ticking time bomb. Notice how I had to use any in the teleport function? That's because Player isn't actually a named class in the traditional sense—it's the result of two function calls returning anonymous classes. If you try to define a proper interface for these mixins, you'll find yourself writing T extends Movable & Destructible & GameObject everywhere.
I've spent way too many hours debugging "Type 'X' is not assignable to type 'Y'" errors caused by mixins. The deeper the mixin chain, the more the TypeScript compiler struggles to track exactly what properties are available on this. It's a pattern that feels like a shortcut but actually adds a layer of cognitive load for everyone on the team.
Replacing Mixins with Composition
Instead of trying to force a class to be multiple things, let's make it have multiple things. This is the "Composition over Inheritance" mantra. Instead of a mixin, we create standalone components that handle specific logic. This keeps our types clean and our classes lean.
interface Position { x: number; y: number; }
class MovementComponent {
position: Position = { x: 0, y: 0 };
move(dx: number, dy: number) {
this.position.x += dx;
this.position.y += dy;
}
}
class HealthComponent {
hp = 100;
damage(amount: number) {
this.hp -= amount;
}
}
class GameObject {
constructor(public name: string) {}
}
class Player extends GameObject {
// Composition: The Player HAS movement and health
movement = new MovementComponent();
health = new HealthComponent();
}
class Wall extends GameObject {
health = new HealthComponent();
}
// Now the type system is trivial and explicit
function teleport(entity: { movement: MovementComponent }) {
entity.movement.move(10, 10);
}
const hero = new Player("Hero");
teleport(hero); // Works perfectly and is type-safe
Enforcing Behaviors with Interfaces
You might be thinking, "But now I have to call hero.movement.move() instead of just hero.move(). That's more typing!" In my experience, that explicit path is actually a feature, not a bug. It tells you exactly where the logic lives.
If you absolutely need a unified API, use an interface. This gives you the contract of a mixin without the inheritance mess. You can delegate the calls to the internal components. This is essentially the Strategy Pattern, and it's far more robust in TypeScript.
interface IMovable {
move(dx: number, dy: number): void;
}
class Player extends GameObject implements IMovable {
private movement = new MovementComponent();
private health = new HealthComponent();
// Delegate the call to the component
move(dx: number, dy: number) {
this.movement.move(dx, dy);
}
}
// Now teleport can take any IMovable, and the compiler is happy
function teleport(entity: IMovable) {
entity.move(10, 10);
}
By moving from mixins to composition and delegation, you've traded "magic" for clarity. Your types are now stable, your components are reusable in isolation, and you aren't fighting the compiler to prove that a class has a certain method.
📋 Practical Task
Build a Modular Combat System
Instead of using mixins to add "Combat" and "Mana" abilities to different game characters, implement a composition-based system.
- Create a
CombatComponentthat handlesattack(target: HealthComponent)and has astrengthproperty. - Create a
ManaComponentthat handlesspendMana(amount: number)and has acurrentManaproperty. - Create a
Characterbase class. - Implement a
Mageclass that has bothCombatComponentandManaComponent. - Implement a
Warriorclass that only hasCombatComponent. - Write a function
performAttack(attacker: { combat: CombatComponent }, target: { health: HealthComponent })that uses the attacker's strength to reduce the target's health.
Constraint: Do not use any function-based mixins or class inheritance for the components themselves. Focus on composition (holding instances of the components as properties).
There are no comments for now.