| |

TypeScript 22 ๐Ÿ”ท Inheritance and Polymorphism

Inheritance is how one class reuses and extends another โ€” a child class gets the parent’s members and can add or override them. Polymorphism is what that enables at use time โ€” the same call behaves differently depending on the actual object, without the caller knowing or caring which subclass it is. Together they’re the backbone of object-oriented TypeScript. Inheritance saves duplication; polymorphism lets you write code against a base type and have it work correctly for every subtype. The two work as a pair, and understanding how they interact in TypeScript โ€” with structural typing, access modifiers, and the override keyword โ€” is what makes them useful rather than fragile.

Key point: Inheritance is the mechanism โ€” class B extends A. Polymorphism is the payoff โ€” a function that takes A and works correctly when passed a B. TypeScript adds three things to the classic model: override for explicit overrides, access modifiers (protected) that interact with inheritance, and structural typing that lets unrelated classes satisfy the same interface. Inheritance and polymorphism are powerful, but composition is often a better default.


What inheritance is

Inheritance lets a class acquire the members of another class and add or change them.

class Animal {
  constructor(public name: string) {}

  speak(): string {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  speak(): string {
    return `${this.name} barks`;
  }

  fetch(): string {
    return `${this.name} fetches the ball`;
  }
}

Dog extends Animal. Dog inherits name and can add fetch(). It overrides speak() โ€” the subclass version replaces the parent’s.

What inheritance gives you:

  • Reuse โ€” the parent’s members are available without re-declaring
  • Extension โ€” the child adds new members
  • Override โ€” the child replaces a member
  • super โ€” access the parent’s constructor and methods

What the child gets:

MemberAccess
public properties and methodsโœ… inherited
protected membersโœ… inherited
private membersโŒ not accessible (but exist)
readonly membersโœ… inherited
Static membersโœ… inherited

The super keyword: Used to call the parent’s constructor and to call overridden methods.

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name);                  // call parent constructor first
  }

  speak(): string {
    return `${super.speak()} โ€” specifically, a bark`;
  }
}

super() must be called before accessing this in a subclass constructor. super.method() calls the parent’s version of an overridden method.

Single inheritance: A class can extend only one parent. That’s the classic model. Multiple inheritance is not supported in TypeScript or JavaScript.

Why inheritance exists: It models “is-a” relationships. A Dog is an Animal. A SavingsAccount is an Account. When the relationship is genuine, inheritance lets you write the shared logic once and specialize it. The trap is using it for “has-a” relationships or for pure code reuse โ€” those lead to fragile hierarchies.


Overriding methods

A subclass can override a parent method. TypeScript requires you to mark it explicitly with override if noImplicitOverride is enabled (part of some strict configs).

class Animal {
  speak(): string {
    return 'generic sound';
  }
}

class Dog extends Animal {
  override speak(): string {
    return 'woof';
  }
}

The override keyword says “I’m intentionally replacing a parent method.” Without it, TypeScript flags the method as suspicious โ€” it might be a typo or an accidental shadow.

Why override matters: If you rename the parent method, the child’s method no longer overrides anything. Without override, it silently becomes a new method โ€” a bug. With override, TypeScript errors, catching the mistake.

class Animal {
  speak(): string { return 'sound'; }
}

class Dog extends Animal {
  override speek(): string {   // โŒ typo โ€” no such method in parent
    return 'woof';
  }
}

The typo fails to compile with override. Without it, speek would be silently added as a new method, and calls to speak() would use the parent’s version.

Return type rules: An override must return the same type or a subtype (covariance).

class Base {
  get(): Animal { return new Animal('x'); }
}

class Sub extends Base {
  override get(): Dog { return new Dog('y', 'lab'); }  // โœ… Dog is an Animal
}

Parameter rules: An override must accept the same parameters or wider (contravariance).

class Base {
  process(x: string | number): void { }
}

class Sub extends Base {
  override process(x: string): void { }  // โŒ too narrow โ€” must accept string | number
}

TypeScript enforces these rules under strictFunctionTypes. They ensure a subclass can be used wherever the parent is expected.

Why overriding rules are strict: If a subclass could narrow parameters or widen return types, code written against the parent would break when given the subclass. The rules enforce the Liskov Substitution Principle โ€” a subclass must be usable anywhere the parent is. TypeScript checks this at compile time.


What polymorphism is

Polymorphism is the ability to use a subclass through its parent’s type. A function that takes Animal works with a Dog without knowing it’s a Dog.

class Animal {
  constructor(public name: string) {}
  speak(): string { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
  override speak(): string { return `${this.name} barks`; }
}

class Cat extends Animal {
  override speak(): string { return `${this.name} meows`; }
}

function describe(animal: Animal): string {
  return animal.speak();   // calls the actual subclass version
}

describe(new Dog('Rex'));  // "Rex barks"
describe(new Cat('Felix'));// "Felix meows"

describe accepts any Animal. When called with a Dog, animal.speak() runs the Dog version. When called with a Cat, it runs the Cat version. The function doesn’t know which โ€” and doesn’t need to.

This is dynamic dispatch: the method called depends on the object’s actual type at runtime, not the declared type of the variable.

Two forms of polymorphism:

  • Subtype polymorphism โ€” using a subclass through its parent’s type (what we just saw)
  • Parametric polymorphism โ€” generics; a function that works for many types

TypeScript supports both. Subtype polymorphism is about class hierarchies; parametric polymorphism is about generics.

Why polymorphism matters: It lets you write general code that works with many specific types. Instead of if (animal instanceof Dog) { ... } else if (...) everywhere, you call animal.speak() and let the object decide. That’s cleaner, extensible, and easier to maintain.

Why “poly” โ€” many, “morph” โ€” forms: The same call takes many forms depending on the object. speak() means one thing to a Dog and another to a Cat, but the caller writes the same code. That uniformity is the value โ€” add a new subclass, and existing code works without changes.


Polymorphism with interfaces

Polymorphism works with interfaces too โ€” often more cleanly than with class hierarchies.

interface Shape {
  area(): number;
}

class Circle implements Shape {
  constructor(private radius: number) {}
  area(): number { return Math.PI * this.radius ** 2; }
}

class Square implements Shape {
  constructor(private side: number) {}
  area(): number { return this.side ** 2; }
}

function totalArea(shapes: Shape[]): number {
  return shapes.reduce((sum, s) => sum + s.area(), 0);
}

Circle and Square are unrelated โ€” neither extends the other. Both implement Shape. The function works with any Shape, regardless of class hierarchy.

Why this is often better: Interfaces decouple the contract from the implementation. Any object with area() works โ€” including plain objects, mocks in tests, or classes from other libraries. Inheritance ties you to a single hierarchy; interfaces don’t.

Structural polymorphism: TypeScript’s structural typing means a class doesn’t even need to declare implements Shape โ€” if it has area(): number, it satisfies the interface.

class Triangle {
  constructor(private base: number, private height: number) {}
  area(): number { return (this.base * this.height) / 2; }
}

const t: Shape = new Triangle(3, 4);  // โœ… structurally compatible

Triangle never declared implements Shape, but it matches the shape, so it works.

Why interface polymorphism is idiomatic: It’s more flexible and less coupled. Inheritance-based polymorphism requires the classes to be related; interface-based polymorphism works for any class with the right shape. When you just need “a thing that does X,” an interface is the tool.


The Liskov Substitution Principle

The Liskov Substitution Principle (LSP) is the rule that makes polymorphism safe: a subclass must be usable anywhere its parent is, without breaking behavior.

What it means in practice:

class Rectangle {
  constructor(public width: number, public height: number) {}
  area(): number { return this.width * this.height; }
}

class Square extends Rectangle {
  constructor(side: number) {
    super(side, side);
  }

  setWidth(w: number): void {
    this.width = w;
    this.height = w;  // must maintain square-ness
  }
}

Is Square a valid subclass of Rectangle? The classic answer is no โ€” because code that expects a Rectangle might set width and height independently, and Square breaks that contract.

function test(r: Rectangle) {
  r.setWidth(5);
  r.setHeight(4);
  console.log(r.area());  // expects 20
}

test(new Rectangle(0, 0));  // 20 โœ…
test(new Square(0));         // 16 โŒ โ€” broke the contract

The violation: Square changed the meaning of setWidth and setHeight. Code that relied on Rectangle‘s behavior fails with Square. That’s a LSP violation.

Why it matters in TypeScript: TypeScript’s type checker can’t catch LSP violations โ€” they’re about behavior, not types. But violating LSP is a real bug source. The rule: if a subclass can’t honor the parent’s contract, it shouldn’t extend the parent.

Alternatives: Use composition. A Square might not be a Rectangle in the substitutable sense โ€” it’s a shape that happens to have a side. Model it as a separate shape or a common interface instead.

Why LSP is subtle: The type system says Square extends Rectangle is valid โ€” Square has all of Rectangle‘s members. But the behavior breaks. LSP is a design principle, not a compiler rule. It’s the mental discipline that keeps inheritance safe.


The fragile base class problem

Inheritance creates tight coupling. A change to the parent can break every subclass โ€” often in ways that are hard to predict.

class Base {
  save(): void {
    this.validate();
    // write to disk
  }

  protected validate(): void {
    // basic validation
  }
}

class Derived extends Base {
  override validate(): void {
    // extended validation that assumes save() behavior
  }
}

If Base.save() is later changed to call validate() twice, or to call it in a different order, Derived might break โ€” even though it wasn’t changed.

The problem: Subclasses depend on the parent’s implementation details, not just its contract. This coupling makes hierarchies fragile.

The mitigation: Design base classes carefully, document what’s overridable, and prefer composition when possible.

The principle: “Favor composition over inheritance.” Instead of subclassing, have a class hold a reference to another class that provides the behavior.

// Composition instead of inheritance
class Engine {
  start(): void { }
}

class Car {
  constructor(private engine: Engine) {}

  start(): void {
    this.engine.start();
  }
}

Car doesn’t extend Engine โ€” it has one. Changing Engine doesn’t break Car the way a parent change would. The coupling is looser.

Why composition is often better: Inheritance is a strong relationship โ€” subclasses are permanently tied to the parent. Composition is a weak relationship โ€” the parts can change independently. For “has-a” relationships, composition is the right model; for “is-a” relationships, inheritance can work. Most modern design favors composition.


Inheritance vs composition

The choice between them is one of the most important in object-oriented design.

AspectInheritanceComposition
Relationship“is-a”“has-a”
CouplingTightLoose
ReuseParent’s codeDelegated behavior
FlexibilitySingle parentMultiple parts
FragilityBase class changesParts independent
TestabilityHard to isolateEasy to mock
Typical useGenuine hierarchiesShared behavior

When inheritance fits:

  • A genuine “is-a” relationship (Dog is an Animal)
  • Shared state and lifecycle
  • A framework that expects subclassing (e.g., Error)
  • Template method pattern โ€” a base class defines an algorithm and subclasses fill in steps

When composition fits:

  • “Has-a” or “uses-a” relationships
  • Reusing behavior from unrelated parts
  • Avoiding the fragile base class problem
  • Multiple sources of behavior
  • Testability matters

A rule of thumb: Start with composition. Reach for inheritance only when the “is-a” relationship is genuine and the hierarchy is stable. Deep hierarchies are a smell; wide, flat compositions are usually better.

Why composition has won in modern practice: Deep inheritance hierarchies are hard to reason about, hard to refactor, and fragile. Composition lets you build systems from independent, testable parts. React, functional programming, and modern OOP all favor composition. Inheritance still has its place โ€” but it’s no longer the default.


A full example

A document processing system showing inheritance, polymorphism, and the trade-offs.

// ============================================
// ABSTRACT BASE CLASS
// ============================================

abstract class Document {
  protected readonly id: string;
  protected readonly createdAt: Date;

  constructor(public readonly title: string) {
    this.id = crypto.randomUUID();
    this.createdAt = new Date();
  }

  abstract render(): string;

  summary(): string {
    return `"${this.title}" (${this.render().length} chars)`;
  }
}

// ============================================
// SUBCLASSES โ€” POLYMORPHISM
// ============================================

class MarkdownDocument extends Document {
  constructor(
    title: string,
    private content: string
  ) {
    super(title);
  }

  override render(): string {
    return this.content;
  }
}

class HtmlDocument extends Document {
  constructor(
    title: string,
    private body: string
  ) {
    super(title);
  }

  override render(): string {
    return `<html><body>${this.body}</body></html>`;
  }

  stripTags(): string {
    return this.body.replace(/<[^>]*>/g, '');
  }
}

// ============================================
// POLYMORPHIC FUNCTION
// ============================================

function renderAll(docs: Document[]): void {
  for (const doc of docs) {
    console.log(doc.summary());
    console.log(doc.render());
  }
}

// ============================================
// INTERFACE-BASED POLYMORPHISM
// ============================================

interface Exportable {
  export(): string;
}

function exportDocs(items: Exportable[]): string[] {
  return items.map(i => i.export());
}

class PdfDocument extends Document implements Exportable {
  constructor(title: string, private pages: number) {
    super(title);
  }

  override render(): string {
    return `PDF with ${this.pages} pages`;
  }

  export(): string {
    return `exported:${this.title}.pdf`;
  }
}

// ============================================
// USAGE
// ============================================

const docs: Document[] = [
  new MarkdownDocument('README', '# Hello'),
  new HtmlDocument('Page', '<p>Hi</p>'),
  new PdfDocument('Report', 12)
];

renderAll(docs);

const exportables: Exportable[] = [
  new PdfDocument('Contract', 5)
];

console.log(exportDocs(exportables));

What this shows:

  • Document abstract base with shared state (id, createdAt, title) and an abstract render()
  • Subclasses override render() โ€” subtype polymorphism
  • renderAll accepts Document[] and calls render() โ€” dynamic dispatch
  • Exportable interface โ€” separate contract for export behavior
  • PdfDocument implements both โ€” multiple contracts

The caller uses Document and Exportable types. The actual behavior depends on the concrete class.

Why this shape: It’s realistic โ€” different document types share metadata and expose different capabilities. Some are Document; some are also Exportable. Functions depend on the smallest contract they need. Inheritance shares metadata; interfaces share capabilities. Both work together.


Complete Example Session

# ============================================
# PART 1: BASIC INHERITANCE
# ============================================

cat > inherit.ts << 'EOF'
class Animal {
  constructor(public name: string) {}

  speak(): string {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name);
  }

  override speak(): string {
    return `${this.name} barks`;
  }

  fetch(): string {
    return `${this.name} (${this.breed}) fetches`;
  }
}

const d = new Dog('Rex', 'Lab');
console.log(d.speak());
console.log(d.fetch());
console.log(d.name);
EOF

npx tsc --noEmit inherit.ts
# (no errors)

# ============================================
# PART 2: POLYMORPHISM
# ============================================

cat > poly.ts << 'EOF'
class Animal {
  constructor(public name: string) {}
  speak(): string { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
  override speak(): string { return `${this.name} barks`; }
}

class Cat extends Animal {
  override speak(): string { return `${this.name} meows`; }
}

function describe(a: Animal): string {
  return a.speak();   // dynamic dispatch
}

const animals: Animal[] = [
  new Dog('Rex'),
  new Cat('Felix'),
  new Animal('Generic')
];

for (const a of animals) console.log(describe(a));
EOF

npx tsc --noEmit poly.ts
# (no errors)

# ============================================
# PART 3: SUPER AND OVERRIDE
# ============================================

cat > super.ts << 'EOF'
class Animal {
  constructor(public name: string) {}
  speak(): string { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
  override speak(): string {
    return `${super.speak()} (specifically, a bark)`;
  }
}

console.log(new Dog('Rex').speak());
EOF

npx tsc --noEmit super.ts
# (no errors)

# ============================================
# PART 4: OVERRIDE ERRORS
# ============================================

cat > override-errors.ts << 'EOF'
class Animal {
  speak(): string { return 'sound'; }
}

class Dog extends Animal {
  override speek(): string { return 'woof'; }  // โŒ typo
}
EOF

npx tsc --noEmit override-errors.ts
# [ override-errors.ts:6:12 - This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'. ]

rm override-errors.ts

# ============================================
# PART 5: INTERFACE POLYMORPHISM
# ============================================

cat > iface.ts << 'EOF'
interface Shape {
  area(): number;
}

class Circle implements Shape {
  constructor(private r: number) {}
  area(): number { return Math.PI * this.r ** 2; }
}

class Square implements Shape {
  constructor(private s: number) {}
  area(): number { return this.s ** 2; }
}

// Structural โ€” no implements needed
class Triangle {
  constructor(private b: number, private h: number) {}
  area(): number { return (this.b * this.h) / 2; }
}

function total(shapes: Shape[]): number {
  return shapes.reduce((sum, s) => sum + s.area(), 0);
}

console.log(total([
  new Circle(2),
  new Square(3),
  new Triangle(4, 5)
]));
EOF

npx tsc --noEmit iface.ts
# (no errors)

# ============================================
# PART 6: ABSTRACT CLASS + POLYMORPHISM
# ============================================

cat > doc.ts << 'EOF'
abstract class Document {
  constructor(public title: string) {}
  abstract render(): string;
  summary(): string { return `${this.title}: ${this.render()}`; }
}

class Markdown extends Document {
  constructor(title: string, private body: string) { super(title); }
  override render(): string { return this.body; }
}

class Html extends Document {
  constructor(title: string, private body: string) { super(title); }
  override render(): string { return `<p>${this.body}</p>`; }
}

const docs: Document[] = [
  new Markdown('A', '# hello'),
  new Html('B', 'hi')
];

for (const d of docs) console.log(d.summary());
EOF

npx tsc --noEmit doc.ts
# (no errors)

# ============================================
# PART 7: COMPILE AND RUN
# ============================================

npx tsc inherit.ts poly.ts super.ts iface.ts doc.ts
node inherit.js
# [ Rex barks ]
# [ Rex (Lab) fetches ]
# [ Rex ]

node poly.js
# [ Rex barks ]
# [ Felix meows ]
# [ Generic makes a sound ]

node super.js
# [ Rex makes a sound (specifically, a bark) ]

node iface.js
# [ 41.132741228718345 ]

node doc.js
# [ A: # hello ]
# [ B: <p>hi</p> ]

Quick Reference

Inheritance Syntax

SyntaxPurpose
class B extends A { }Inherit from A
super()Call parent constructor
super.method()Call parent method
override method()Override parent method
class B extends A implements I { }Inherit and implement

Access in Subclasses

MemberInherited?Accessible?
publicโœ…โœ…
protectedโœ…โœ…
privateโœ… existsโŒ not accessible
readonlyโœ…โœ… (read)
staticโœ…โœ… via class name
Constructorโœ…via super()

Override Rules

RuleDetail
Use overrideRequired under noImplicitOverride
Return typeSame or subtype (covariant)
ParametersSame or wider (contravariant)
Access modifierCan’t narrow (public โ†’ protected โŒ)
super accessCan call parent version

Polymorphism Types

TypeMechanism
SubtypeClass hierarchy โ€” Dog as Animal
InterfaceStructural โ€” any shape match
ParametricGenerics โ€” <T>

super Rules

RuleDetail
Call before thisRequired in subclass constructor
Call onceOnly one super() per constructor
Access parent methodsuper.method()
Access parent propertyNot via super โ€” use this

Liskov Substitution

RuleMeaning
BehavioralSubclass must honor parent’s contract
Return typeCovariant (narrower)
ParametersContravariant (wider)
PreconditionsCan’t strengthen
PostconditionsCan’t weaken

Inheritance vs Composition

AspectInheritanceComposition
Relationship“is-a”“has-a”
CouplingTightLoose
FlexibilitySingle parentMultiple parts
FragilityHighLow
TestabilityHarderEasier
DefaultโŒโœ…

When to Inherit

FitNot fit
Genuine “is-a”“has-a”
Shared state + behaviorPure code reuse
Template methodUnrelated classes
Framework subclassingDeep hierarchies
Stable hierarchyRapidly changing base

Common Patterns

PatternStructure
Template methodAbstract class + hooks
StrategyInterface + multiple impls
FactoryStatic method returning subclass
DecoratorComposition wrapping
AdapterWrapper implementing target

Error Cases

ErrorCause
Cannot access 'this' before super()super() not first
must call super()Missing constructor super()
is not declared in base classoverride without parent
is not assignableLSP violation of return type
Cannot overrideModifier narrowing

Static Inheritance

AspectBehavior
Inheritedโœ…
Accessible via subclassโœ…
this in staticRefers to subclass
OverridePossible but rare

Best Practices

โœ… Do This:

// Use inheritance for genuine "is-a" relationships
class Dog extends Animal { }                              // โœ…

// Mark overrides explicitly
override speak(): string { return 'woof'; }               // โœ…

// Call super in constructors
constructor(name: string) {
  super(name);
  // ...
}                                                          // โœ…

// Use protected for subclass-only access
protected log(msg: string): void { }                      // โœ…

// Accept the base type in functions for polymorphism
function describe(a: Animal): string { return a.speak(); } // โœ…

// Prefer interface polymorphism for decoupling
interface Shape { area(): number; }                       // โœ…

// Favor composition for "has-a"
class Car {
  constructor(private engine: Engine) {}
}                                                          // โœ…

// Keep hierarchies shallow
class Animal { }
class Dog extends Animal { }                              // โœ… (2 levels)

// Document overridable members
protected hook(): void { }                                // โœ…

โŒ Don’t Do This:

// Don't use inheritance for code reuse alone
class Utils extends MathHelpers { }  // โŒ "is-a" false        // โŒ

// Don't forget override
speak(): string { return 'woof'; }  // โš ๏ธ  silent override      // โš ๏ธ

// Don't access this before super
constructor() {
  this.x = 1;  // โŒ
  super();
}                                                          // โŒ

// Don't use private when subclasses need access
class Base { private helper(): void { } }  // โš ๏ธ  use protected // โš ๏ธ

// Don't build deep hierarchies
class A extends B extends C extends D extends E { }        // โŒ

// Don't narrow parameter types in overrides
override process(x: string): void { }  // โš ๏ธ  must be wider   // โš ๏ธ

// Don't break LSP
class Square extends Rectangle { }  // โš ๏ธ  if behavior differs // โš ๏ธ

// Don't use inheritance for mixins
class A extends B extends C { }  // โŒ can't combine behaviors  // โŒ

Common Pitfalls

PitfallProblemSolution
this before super()Runtime/compile errorCall super() first
Missing overrideSilent shadowAdd override
Private parent memberInaccessibleUse protected
Narrowing override paramsCompile errorWiden, don’t narrow
LSP violationRuntime bugsHonor parent contract
Deep hierarchiesFragile, hard to reasonPrefer composition
Inheritance for reuse onlyFalse “is-a”Use composition
Overriding access modifierCan’t narrowKeep or widen
Forgetting static inheritanceWrong thisUnderstand this in static
Single inheritance limitsCan’t combineUse interfaces

Real-World Examples

1. Basic inheritance

class Animal { }
class Dog extends Animal { }

2. Subclass constructor with super

class Dog extends Animal {
  constructor(name: string) { super(name); }
}

3. Override method

class Dog extends Animal {
  override speak(): string { return 'woof'; }
}

4. Call parent method

class Dog extends Animal {
  override speak(): string {
    return `${super.speak()}!`;
  }
}

5. Protected member

class Base {
  protected log(msg: string): void { }
}

6. Polymorphic function

function speak(a: Animal): string { return a.speak(); }

7. Polymorphic array

const animals: Animal[] = [new Dog(), new Cat()];

8. Interface polymorphism

interface Shape { area(): number; }
function total(shapes: Shape[]): number { }

9. Structural compatibility

class Triangle { area(): number { return 0; } }
const s: Shape = new Triangle();  // โœ… if shape matches

10. Abstract + polymorphism

abstract class Shape {
  abstract area(): number;
}
function describe(s: Shape): string { }

11. Template method

abstract class Processor {
  process(): void {
    this.step1();
    this.step2();
  }
  protected abstract step1(): void;
  protected abstract step2(): void;
}

12. instanceof narrowing

function describe(a: Animal): string {
  if (a instanceof Dog) return a.fetch();
  return a.speak();
}

13. Mixin via composition

class Logger {
  log(msg: string): void { }
}
class Service {
  constructor(private logger: Logger) {}
}

14. Factory returning subclass

class Shape {
  static create(type: 'circle' | 'square'): Shape {
    return type === 'circle' ? new Circle() : new Square();
  }
}

15. Static inheritance

class Base {
  static name = 'base';
}
class Sub extends Base { }
Sub.name;  // inherited

16. Abstract with shared state

abstract class Entity {
  protected id = crypto.randomUUID();
  abstract save(): Promise<void>;
}

17. Multiple interfaces

class User implements Serializable, Comparable<User> { }

18. Covariant return

class Base { get(): Animal { } }
class Sub extends Base { override get(): Dog { } }  // โœ…

19. Contravariant params

class Base { accept(x: string | number): void { } }
class Sub extends Base { override accept(x: string | number | boolean): void { } }  // โœ…

20. Composition over inheritance

class Engine { start(): void { } }
class Car {
  constructor(private engine: Engine) {}
  start(): void { this.engine.start(); }
}

Visual: Inheritance

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Animal                                โ”‚
โ”‚  โ”€ name                                      โ”‚
โ”‚  โ”€ speak()                                   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                    โ–ฒ
                    โ”‚ extends
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚                       โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Dog    โ”‚      โ”‚  class Cat   โ”‚
โ”‚  โ”€ breed      โ”‚      โ”‚  โ”€ indoor    โ”‚
โ”‚  โ”€ speak()    โ”‚      โ”‚  โ”€ speak()   โ”‚
โ”‚  โ”€ fetch()    โ”‚      โ”‚  โ”€ purr()    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Dog is-a Animal
Cat is-a Animal

Visual: Polymorphism

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  function describe(a: Animal): string {      โ”‚
โ”‚    return a.speak();                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  called with
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  describe(new Dog('Rex'))                    โ”‚
โ”‚  โ†’ animal is actually Dog                    โ”‚
โ”‚  โ†’ a.speak() runs Dog.speak()                โ”‚
โ”‚  โ†’ "Rex barks"                               โ”‚
โ”‚                                              โ”‚
โ”‚  describe(new Cat('Felix'))                  โ”‚
โ”‚  โ†’ animal is actually Cat                    โ”‚
โ”‚  โ†’ a.speak() runs Cat.speak()                โ”‚
โ”‚  โ†’ "Felix meows"                             โ”‚
โ”‚                                              โ”‚
โ”‚  Same call, different behavior               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: super Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Dog extends Animal {                  โ”‚
โ”‚    constructor(name: string) {               โ”‚
โ”‚      super(name);                            โ”‚
โ”‚      // โ†‘ runs Animal's constructor          โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Dog extends Animal {                  โ”‚
โ”‚    override speak() {                        โ”‚
โ”‚      return super.speak() + '!';             โ”‚
โ”‚      //     โ†‘ calls Animal.speak()           โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Rules:                                      โ”‚
โ”‚  โ”€ super() before this in constructor        โ”‚
โ”‚  โ”€ only one super() call                     โ”‚
โ”‚  โ”€ super.method() for parent methods         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Inheritance vs Composition

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Inheritance โ€” "is-a"                        โ”‚
โ”‚                                              โ”‚
โ”‚  class Dog extends Animal { }                โ”‚
โ”‚                                              โ”‚
โ”‚  Dog gets all of Animal's members            โ”‚
โ”‚  Tightly coupled                             โ”‚
โ”‚  Single parent                               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Composition โ€” "has-a"                       โ”‚
โ”‚                                              โ”‚
โ”‚  class Car {                                 โ”‚
โ”‚    constructor(private engine: Engine) {}    โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Car delegates to engine                     โ”‚
โ”‚  Loosely coupled                             โ”‚
โ”‚  Multiple parts                              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Liskov Substitution

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Rectangle {                           โ”‚
โ”‚    setWidth(w: number): void { }             โ”‚
โ”‚    setHeight(h: number): void { }            โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  class Square extends Rectangle {            โ”‚
โ”‚    // setting width also sets height         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Code expecting Rectangle:                   โ”‚
โ”‚                                              โ”‚
โ”‚  r.setWidth(5);                              โ”‚
โ”‚  r.setHeight(4);                             โ”‚
โ”‚  area = 20  โ† expects this                   โ”‚
โ”‚                                              โ”‚
โ”‚  With Square:                                โ”‚
โ”‚  area = 16  โ† โŒ LSP violation               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Override Rules

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Return type โ€” covariant (narrower) โœ…       โ”‚
โ”‚                                              โ”‚
โ”‚  Base: get(): Animal                         โ”‚
โ”‚  Sub:  get(): Dog                            โ”‚
โ”‚                                              โ”‚
โ”‚  Dog is assignable to Animal                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Parameters โ€” contravariant (wider) โœ…       โ”‚
โ”‚                                              โ”‚
โ”‚  Base: accept(x: string)                     โ”‚
โ”‚  Sub:  accept(x: string | number)            โ”‚
โ”‚                                              โ”‚
โ”‚  Sub accepts more, not less                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Modifier โ€” can't narrow โŒ                  โ”‚
โ”‚                                              โ”‚
โ”‚  Base: public method()                       โ”‚
โ”‚  Sub:  protected method()  โŒ                โ”‚
โ”‚                                              โ”‚
โ”‚  Sub can't restrict access                   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: override Safety

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  With `override`:                            โ”‚
โ”‚                                              โ”‚
โ”‚  class Dog extends Animal {                  โ”‚
โ”‚    override speek() { }                      โ”‚
โ”‚    // โŒ error โ€” no 'speek' in Animal        โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Typo caught at compile time                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Without `override`:                         โ”‚
โ”‚                                              โ”‚
โ”‚  class Dog extends Animal {                  โ”‚
โ”‚    speek() { }                               โ”‚
โ”‚    // โš ๏ธ  silently added as new method       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Calls to speak() use Animal's version       โ”‚
โ”‚  Bug hidden                                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Composition Chain

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Instead of deep inheritance:                โ”‚
โ”‚                                              โ”‚
โ”‚  class A extends B extends C extends D { }   โ”‚
โ”‚                                              โ”‚
โ”‚  Use composition:                            โ”‚
โ”‚                                              โ”‚
โ”‚  class Service {                             โ”‚
โ”‚    constructor(                              โ”‚
โ”‚      private logger: Logger,                 โ”‚
โ”‚      private db: Database,                   โ”‚
โ”‚      private cache: Cache                    โ”‚
โ”‚    ) {}                                      โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Each part is independent and testable       โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Decision Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Is it a genuine "is-a" relationship?        โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ Yes โ”€โ”€โ–บ Stable hierarchy?          โ”‚
โ”‚       โ”‚            โ”‚                         โ”‚
โ”‚       โ”‚            โ”œโ”€โ”€ Yes โ”€โ”€โ–บ Inheritance   โ”‚
โ”‚       โ”‚            โ”‚                         โ”‚
โ”‚       โ”‚            โ””โ”€โ”€ No  โ”€โ”€โ–บ Composition   โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ No โ”€โ”€โ–บ Composition                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Do you need shared behavior?                โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ Yes โ”€โ”€โ–บ Template method (abstract) โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ No โ”€โ”€โ–บ Interface + composition     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConceptMeaning
InheritanceSubclass extends parent
extendsInherit from a class
super()Call parent constructor
super.method()Call parent method
overrideExplicit override marker
PolymorphismSame call, different behavior
Subtype polymorphismUse subclass as parent
Interface polymorphismUse any matching shape
Covariant returnNarrower return type allowed
Contravariant paramsWider parameter type allowed
LSPSubclass must be substitutable
Composition“Has-a” instead of “is-a”

Key takeaways:

  • Inheritance lets a class extend another and reuse or override its members
  • extends is single inheritance โ€” a class can have only one parent
  • super() must be called before this in a subclass constructor
  • override makes overriding explicit and catches typos
  • Polymorphism lets code use a subclass through its parent’s type
  • Dynamic dispatch calls the actual object’s method, not the declared type’s
  • Interface polymorphism is more flexible than class hierarchies โ€” any matching shape works
  • Liskov Substitution โ€” a subclass must honor the parent’s contract or it breaks callers
  • Return types can narrow (covariant); parameters can widen (contravariant)
  • The fragile base class problem โ€” parent changes can break subclasses
  • Favor composition over inheritance โ€” “has-a” is usually safer than “is-a”
  • Keep hierarchies shallow โ€” deep inheritance is hard to reason about
  • Use inheritance for genuine is-a relationships and shared behavior; use interfaces and composition for everything else

Remember: Inheritance is a tool for expressing “is-a” relationships and sharing behavior across related classes. Polymorphism is what makes that hierarchy useful โ€” the same call behaves differently per object, without the caller knowing. But inheritance creates tight coupling and fragile hierarchies. Use it deliberately, mark overrides explicitly, honor the Liskov Substitution Principle, and reach for composition when the relationship isn’t genuinely “is-a.” The result is code that’s easier to extend, test, and reason about.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!