Inheritance vs Composition in TypeScript

Inheritance shares behavior through parent classes. Composition builds objects by combining smaller pieces. Learn when each pattern works best in TypeScript.

7 min read

TypeScript inheritance vs composition comes down to how a class gets its behavior. Inheritance means one class gets all the members of another class through the extends keyword. Composition means a class receives its capabilities from objects passed into it, typically through the constructor.

Here is the same problem solved both ways. The goal is to model a system where different types of workers can perform different tasks.

The Inheritance Approach

typescripttypescript
class Worker {
  constructor(public name: string) {}
  work(): string { return `${this.name} is working.`; }
}
class Developer extends Worker {
  code(): string { return `${this.name} is coding.`; }
}
class Designer extends Worker {
  design(): string { return `${this.name} is designing.`; }
}

Both Developer and Designer inherit work from Worker through extends, and each adds its own specific method. Now create a Developer and call both the inherited and the own method:

typescripttypescript
const dev = new Developer("Ada");
console.log(dev.work());
console.log(dev.code());

This prints both lines below, since work comes from the Worker base class and code is defined directly on Developer.

texttext
Ada is working.
Ada is coding.

This works fine for two roles. The hierarchy is shallow and clear. Developer is-a Worker, and it gets the work method for free.

The problem shows up when you need a Developer who also designs, or a Designer who also codes. With inheritance, you cannot easily combine capabilities from two different branches of the hierarchy.

The Composition Approach

typescripttypescript
interface CanCode { code(): string; }
interface CanDesign { design(): string; }
 
class Coder implements CanCode {
  constructor(private name: string) {}
  code(): string { return `${this.name} is coding.`; }
}
class VisualDesigner implements CanDesign {
  constructor(private name: string) {}
  design(): string { return `${this.name} is designing.`; }
}

Each capability is a focused class with a single responsibility. Coder only knows how to code, and VisualDesigner only knows how to design. Now a hybrid class can combine both by holding references to each:

typescripttypescript
class CreativeDeveloper implements CanCode, CanDesign {
  private coder = new Coder(this.name);
  private designer = new VisualDesigner(this.name);
  constructor(private name: string) {}
  code(): string { return this.coder.code(); }
  design(): string { return this.designer.design(); }
}

CreativeDeveloper holds a Coder and a VisualDesigner as private fields, then delegates each interface method to the matching composed object. Calling both methods on an instance shows that it satisfies both capabilities:

typescripttypescript
const hybrid = new CreativeDeveloper("Ada");
console.log(hybrid.code());
console.log(hybrid.design());

This prints both lines below, since each call is forwarded to the composed coder or designer instance instead of relying on inheritance.

texttext
Ada is coding.
Ada is designing.

CreativeDeveloper combines two behaviors without inheriting from either. It delegates to composed objects rather than extending a parent class. You can add a third capability (testing, managing, documenting) without changing the existing classes at all.

When Inheritance Works Well

Inheritance is the right choice when the relationship is genuinely hierarchical and unlikely to change. The classic example is UI components: a Button is a Control, a TextBox is a Control, and both need shared positioning and styling logic that the base class provides.

Inheritance also makes sense when you need to pass a group of objects through a shared base type. If every payment method must be a PaymentProcessor with a charge method, inheritance enforces that contract through the type system.

When Composition Works Better

Composition shines when capabilities need to be mixed and matched. A game character might need to walk, fly, swim, or teleport. With composition, you attach movement behaviors at runtime rather than locking them into a class hierarchy at design time.

Composition also makes unit testing easier. You can pass a mock engine into a Car constructor to test the car in isolation. With inheritance, testing a subclass often requires setting up the entire parent chain.

Side-by-Side Comparison

InheritanceComposition
Relationshipis-ahas-a
CouplingTight (child depends on parent)Loose (depends on interfaces)
FlexibilityChanges to parent affect all childrenSwap parts independently
Code reuseInherit methods automaticallyDelegate to composed objects
Multiple sourcesOne parent class onlyUnlimited composed objects
TestabilityHarder (must set up parent)Easier (mock dependencies)

A Practical Rule of Thumb

Start with composition. Write small, focused classes that do one thing. If you later notice multiple classes sharing identical behavior with a clear is-a relationship, extract an abstract base class through inheritance.

The TypeScript type system supports both patterns equally well. There is no performance difference: both compile to the same JavaScript prototype delegation. The choice is about design, not mechanics.

For more on building flexible class structures, see abstract classes in TypeScript and implementing interfaces with TypeScript classes. For practical class modeling, see model objects with TypeScript classes.

Rune AI

Rune AI

Key Insights

  • Inheritance uses extends to share behavior through a parent class. Composition passes behavior objects into a constructor.
  • Inheritance creates tight coupling between parent and child. Composition creates loose coupling through interfaces.
  • Composition makes it easier to unit test because you can pass mock dependencies into the constructor.
  • Inheritance is best for stable is-a relationships. Composition is best for flexible has-a relationships.
  • TypeScript supports both patterns natively. You can mix them in the same class when needed.
RunePowered by Rune AI

Frequently Asked Questions

Is inheritance bad in TypeScript?

Inheritance is not bad, but it can lead to rigid class hierarchies that are hard to change later. Composition is often more flexible because you can swap behaviors at runtime and avoid deep inheritance chains. Both patterns have valid use cases.

Can I mix inheritance and composition in the same class?

Yes. A class can extend a base class for shared identity while also accepting composed objects through its constructor. This is common in real-world TypeScript codebases.

What does composition over inheritance actually mean?

It means preferring to build objects by combining smaller, focused pieces rather than creating deep parent-child class hierarchies. In TypeScript, you achieve composition by passing dependencies into a class constructor or using interfaces to define capabilities.

Conclusion

Inheritance models an is-a relationship: a Dog is an Animal. Composition models a has-a relationship: a Car has an Engine. Both patterns exist in TypeScript and both compile to the same JavaScript prototype chain. Choose inheritance when the relationship is truly hierarchical and stable. Choose composition when you want flexibility, testability, and the ability to swap behaviors without changing class definitions.