Composition

Inheritance creates an “is-a” relationship: a GradStudent is a Student. Composition creates a “has-a” relationship: a Car has an Engine. Both are ways to reuse code and build complex objects, but they have different trade-offs.

The Problem with Deep Inheritance

Suppose we are modeling different types of employees:

class Employee {
  constructor(name) {
    this.name = name;
  }
  work() {
    console.log(`${this.name} is working`);
  }
}

class Manager extends Employee {
  manage() {
    console.log(`${this.name} is managing`);
  }
}

class Developer extends Employee {
  code() {
    console.log(`${this.name} is coding`);
  }
}

This works until you need a TechLead who can both manage and code. JavaScript does not support multiple inheritance, so you can not extend both Manager and Developer. You might try:

class TechLead extends Manager {
  code() {
    console.log(`${this.name} is coding`);
  }
}

But now you have duplicated the code method. The more requirements you add, the more rigid the inheritance hierarchy gets, and the harder it is to change.

Composition as an Alternative

With composition, you build objects by combining smaller, focused pieces:

const canWork = (state) => ({
  work: () => console.log(`${state.name} is working`),
});

const canManage = (state) => ({
  manage: () => console.log(`${state.name} is managing`),
});

const canCode = (state) => ({
  code: () => console.log(`${state.name} is coding`),
});

function createEmployee(name) {
  const state = { name };
  return { ...state, ...canWork(state) };
}

function createManager(name) {
  const state = { name };
  return { ...state, ...canWork(state), ...canManage(state) };
}

function createDeveloper(name) {
  const state = { name };
  return { ...state, ...canWork(state), ...canCode(state) };
}

function createTechLead(name) {
  const state = { name };
  return {
    ...state,
    ...canWork(state),
    ...canManage(state),
    ...canCode(state),
  };
}

const alice = createTechLead("Alice");
alice.work();  // Alice is working
alice.manage();  // Alice is managing
alice.code();  // Alice is coding

Each capability (canWork, canManage, canCode) is defined once and mixed into objects as needed. Adding a new combination does not require changing existing code.

Composition with Classes

You can also use composition with classes by injecting dependencies:

class Logger {
  log(message) {
    console.log(`[LOG] ${message}`);
  }
}

class EmailService {
  send(to, message) {
    console.log(`Sending email to ${to}: ${message}`);
  }
}

class NotificationService {
  constructor(logger, emailService) {
    this.logger = logger;
    this.emailService = emailService;
  }

  notify(user, message) {
    this.logger.log(`Notifying ${user}`);
    this.emailService.send(user, message);
  }
}

const logger = new Logger();
const emailService = new EmailService();
const notifications = new NotificationService(logger, emailService);

notifications.notify("alice@example.com", "Hello!");

Here, NotificationService does not inherit from Logger or EmailService. It uses them instead. This makes the code more flexible: you can easily swap in a different logger or email service without changing NotificationService.

When to Use Each

Prefer inheritance when:

  • There is a clear, stable “is-a” relationship
  • You want to leverage polymorphism (treating subclasses as their parent type)
  • The hierarchy is shallow (one or two levels)

Prefer composition when:

  • You need to combine behaviors from multiple sources
  • The relationships might change over time
  • You want loose coupling between components
  • You are modeling “has-a” or “uses-a” relationships