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:
- Plain objects (data only, no behavior)
- 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