Asher Cohen
Back to posts

Inheritance vs Composition in JavaScript

Why composition often beats inheritance for flexible, maintainable JavaScript code

Why I No Longer Write Classes in JavaScript

I don't need them.

Instead, I use:

  1. Plain objects (data only, no behavior)
  2. Pure functions (no side-effects, no outside dependencies)

Easy to test. ✅

Easy to understand. 🧠

Easy to compose. 🏘

The Problem with Inheritance

Classical inheritance creates tight coupling between parent and child classes. Changes in the parent can break children in unexpected ways—the fragile base class problem.

// Inheritance creates tight coupling
class Animal {
  move() {
    /* ... */
  }
}

class Dog extends Animal {
  // What if Dog needs different movement logic?
}

Issues:

  • Rigid hierarchies
  • Accidental complexity
  • Hard to change at runtime
  • Multiple inheritance not supported

The Power of Composition

Composition builds behavior by combining simple, independent functions.

// Composition with functions
const mover = (obj) => ({
  ...obj,
  move: (distance) => ({ ...obj, position: obj.position + distance }),
});

const speaker = (obj) => ({
  ...obj,
  speak: (sound) => console.log(sound),
});

const dog = compose({ name: 'Buddy', position: 0 }, mover, speaker);

Benefits:

  • Flexible and modular
  • Easy to test in isolation
  • Runtime composition possible
  • No hierarchy constraints

When to Use Each

Inheritance (Rarely)

  • Clear "is-a" relationships
  • Shared base behavior with minimal variation
  • Framework requirements (e.g., React components pre-hooks)

Composition (Mostly)

  • Building complex behavior from simple parts
  • Need runtime flexibility
  • Avoiding tight coupling
  • Mixing and matching capabilities

Practical Examples

Strategy Pattern with Composition

// Different sorting strategies
const quickSort = (arr) => {
  /* ... */
};
const mergeSort = (arr) => {
  /* ... */
};

const sorter = (strategy) => ({
  sort: (data) => strategy(data),
});

const quickSorter = sorter(quickSort);
const mergeSorter = sorter(mergeSort);

Mixin Pattern

const withLogging = (obj) => ({
  ...obj,
  log: (msg) => console.log(`[${obj.name}] ${msg}`),
});

const withValidation = (obj) => ({
  ...obj,
  validate: () => {
    /* ... */
  },
});

Conclusion

Favor composition over inheritance. It leads to more flexible, testable, and maintainable code. Use classes sparingly and only when they genuinely simplify your design.

#javascript #composition #software-architecture