Skip to Content
Course content

203: Alternative Patterns to Mixins

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 CombatComponent that handles attack(target: HealthComponent) and has a strength property.
  • Create a ManaComponent that handles spendMana(amount: number) and has a currentMana property.
  • Create a Character base class.
  • Implement a Mage class that has both CombatComponent and ManaComponent.
  • Implement a Warrior class that only has CombatComponent.
  • 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).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.