What you'll be able to do
By the end of this page you should be able to spot a deep class tree that will hurt later, model the same feature with composition (small pieces combined), and pick inheritance only when "is-a" truly fits.
Previous post: classes, extends, and super.
Who this is for
- People who wrote
Admin extends User extends Person extends Entityand felt stuck - Beginners copying OOP tutorials that subclass everything
- Anyone building UI or game objects that need mixed behaviors (fly + swim + log)
You can skip this if you already favor small modules over five-level extends. This is judgment, not syntax memorization.
Inheritance — one chain, shared prototype
class Bird {
fly() { return "flap"; }
}
class Penguin extends Bird {
// penguins don't fly — awkward override coming
}
Scenario A — true "is-a". Admin extends User when every admin is a user with extra fields — reasonable.
Scenario B — mixed abilities. A logging, payable, exportable invoice is not cleanly one linear chain; forcing it creates empty overrides and fragile super calls.
The rule: inheritance excels when behavior truly layers in one dimension; it struggles when objects need arbitrary bundles of features.
Composition — build by combining parts
function canLog(obj) {
return {
log(msg) {
console.log("[" + obj.name + "] " + msg);
},
};
}
function createUser(name) {
const user = { name };
return Object.assign(user, canLog(user));
}
Scenario A — feature flags. Add canExport, canPay helpers and merge what you need per object.
Scenario B — React later. Hooks and plain functions often compose; giant class hierarchies are less common in modern UI code.
Modern style also uses class fields with injected dependencies (this.logger = logger) — still composition: the object has a logger, it does not extend Logging.
When each fits (practical, not dogma)
| Situation | Often works |
|---|---|
| Shared UI base with one override | shallow extends |
| Object needs 3 unrelated behaviors | composition |
Framework requires extends Component | follow the framework, keep your logic small inside |
Scenario A — DOM widget. A Modal has a focus trap and has a close button handler — compose helpers.
Scenario B — typed domain model. Square extends Rectangle can be fine if the math genuinely specializes — but stop before unrelated cross-links.
A tiny practice file
<!DOCTYPE html>
<html>
<body>
<button id="go">Compose user</button>
<pre id="out"></pre>
<script>
function withGreet(obj) {
return {
greet() {
return "Hello, " + obj.name;
},
};
}
function withBadge(obj, badge) {
return {
badge() {
return obj.name + " · " + badge;
},
};
}
function createMember(name) {
const base = { name };
return Object.assign(base, withGreet(base), withBadge(base, "member"));
}
document.getElementById("go").addEventListener("click", function () {
const m = createMember("Asha");
document.getElementById("out").textContent =
m.greet() + "\n" + m.badge();
});
</script>
</body>
</html>
No extends — two behaviors merged onto one object.
Mistakes I see a lot
1. Subclassing only to reuse three lines. Extract a function instead.
2. Deep trees where sibling classes duplicate code. Composition or shared utilities fix this faster.
3. "Composition vs inheritance" as a religion. Shallow inheritance plus composition inside is normal.
4. Mocking inheritance in interviews but never using composition in projects. Try both in tiny files so your gut recognizes the smell of a fourth extends.
What to try before the next post
- Sketch a bad four-level
extendstree; rewrite with two composed helpers. - List "has-a" vs "is-a" for one domain object you know (cart, player, invoice).
- Build the tiny HTML file.
Next in this series: closures with real examples — functions that remember outer variables.
Try this next outside the series
These mental models become more useful when you can point at a real framework problem they explain.
- React Server Components explained — see execution boundaries and composition decisions in a modern app
- Node.js error handling — watch call stacks and thrown errors matter in real request code