Clean Code

Prefer Composition Over Inheritance in TypeScript: Best Practices and Examples

I once inherited a codebase with a class hierarchy six levels deep. BaseEntity → TimestampedEntity → SoftDeletableEntity → AuditableEntity → TenantAwareEn...

19 Apr 2024

Prefer Composition Over Inheritance in TypeScript: Best Practices and Examples

I once inherited a codebase with a class hierarchy six levels deep. BaseEntityTimestampedEntitySoftDeletableEntityAuditableEntityTenantAwareEntityProduct. Changing anything in BaseEntity broke half the system. Adding a new type of entity meant deciding which branch of the tree to extend — and getting it wrong.

That's the inheritance trap. It feels natural at first. Then it calcifies.

What composition means

Composition builds objects by combining smaller, focused pieces. Instead of saying "a Car is a Vehicle," you say "a Car has an Engine and has Wheels."

Typescript
class Engine {
  start(): void {
    console.log('Engine started');
  }
}

class Wheel {
  rotate(): void {
    console.log('Wheel rotating');
  }
}

class Car {
  private engine: Engine;
  private wheels: Wheel[];

  constructor() {
    this.engine = new Engine();
    this.wheels = [new Wheel(), new Wheel(), new Wheel(), new Wheel()];
  }

  start(): void {
    this.engine.start();
    this.wheels.forEach(wheel => wheel.rotate());
    console.log('Car started');
  }
}

const car = new Car();
car.start();

No inheritance chain. Each piece is independent. You can swap the Engine for an ElectricEngine without touching Car's parent class — because there is no parent class.

Why inheritance becomes a problem

Tight coupling. A child class depends on every implementation detail of its parent. Change the parent, and every child is affected.

The fragile base class problem. Modify a method in a base class and you break subclasses that override it, or worse, subclasses that silently depend on its behavior.

Rigid hierarchies. Real-world objects don't fit neatly into trees. A FlyingCar doesn't know whether to extend Car or Aircraft. With composition, it just has both capabilities.

God classes. Base classes tend to accumulate shared functionality over time. They grow fat with methods that only some children use.

When to use composition

Use composition when you want to combine behaviors. Use it when different objects share some capabilities but not an identity.

Think of it like LEGO blocks. Each block is small and self-contained. You assemble them into whatever shape you need. Tomorrow you can rearrange them.

When inheritance is still fine

Inheritance works when you have a genuine "is-a" relationship and the hierarchy is shallow — one, maybe two levels. An HttpError extending Error makes sense. A six-level entity hierarchy doesn't.

The trade-off

Composition requires more explicit wiring. You write more constructor code. You manually delegate method calls. It's more verbose than extends BaseEntity.

But that verbosity buys you flexibility. You can change, swap, and test individual pieces without touching the rest. In my experience, that trade is worth it every time the hierarchy goes past two levels.

Default to composition. Reach for inheritance only when it genuinely simplifies things.

Keep reading