Skip to content

Inheritance didn't deserve the exile

Inheritance didn't deserve the exile

"Composition over inheritance" began as a correction to a real disease — ten-level hierarchies where changing a base class rippled fear through a codebase. The correction was right. The sentence that followed was not: somewhere along the way, preferring composition became outlawing inheritance, and frontend spent a decade rebuilding inheritance's use cases — shared behavior, refinement, specialization — out of spread operators and convention.

Here is what the exile cost, in one working example. A pricing chain: Product knows its price, SaleProduct refines the total through super, TaxedProduct refines it again — three classes in three files, every level reactive, every level usable on its own:

ts
class $TaxedProduct extends SaleProduct.$Class {
  get taxRate() {
    return ref(0.1);
  }
  get total(): number {
    return super.total * (1 + this.taxRate.value); // reactive through the chain
  }
}

total is a plain-getter chain — three levels deep, zero computed() allocations — and writing to the grandparent's ref re-derives the whole thing. Each level also appends its own line to a receipt through ordinary super.receipt() calls. Live, from the exact files the docs ship:

Three files, three levels, one instanceLive · runs the shipped engine
Mechanical keyboard — $48.00
sale −20%
tax +10%
due — $42.24
price · Product
$48
discount · SaleProduct
20%
tax · TaxedProduct
10%
total · plain getter
$42.24
total is a plain-getter chain — each level refines super.total, zero computeds allocated. Every receipt() line is written by a different class in the chain. Write to any level's ref and everything re-derives.

Try expressing that with composition: you'll write the plumbing that extends is. Refinement of behavior through layers is inheritance's home turf — the abuse was depth without design, not the mechanism.

The engine's part is making it safe where earlier attempts collapsed: each (prototype, key) pair caches under its own symbol, so a parent's computed and a child's same-name computed coexist on one instance instead of colliding — super.x.value just works, at any depth (Inheritance & super).

Use composition where parts assemble. Use inheritance where behavior refines. Owning both is the point of having a language with classes in it.

Released under the MIT License.