Beginner12 min readObject-Oriented Foundationslive prototype

Inheritance

A subclass reuses and extends a parent class through an "is-a" relationship — inheriting fields and methods, and overriding only what it needs to change.

The idea

What it is

Inheritance lets one class build on another. You write a general parent class once, then create child classes that automatically get all of its fields and methods — and add or change only what's different. Less copy-paste, one place to fix bugs.

Think of animals. Every animal has a name, can eat(), and can sleep(). A dog is an animal, so it gets all of that for free — but it also barks instead of making a generic noise, and it can fetch(). A cat is an animal too: same shared behavior, but it meows and can scratch(). You describe the shared parts once in Animal, then let each species specialize.

The one sentence to remember

A subclass inherits what the parent already does, adds new behavior of its own, and overrides the few methods it wants to do differently. Reuse plus specialization, in one relationship.

Mechanics

How it works

Base class vs. derived class

The base class (also called the parent or superclass) holds the shared, general behavior — here, Animal. A derived class (the child or subclass) extends it — Dog, Cat, Bird. The derived class starts with everything the base has, then layers its own changes on top.

In most languages you spell this with the extends keyword. class Dog extends Animal reads almost like English: a Dog extends what an Animal already is.

Inheriting fields and methods

Because Dog extends Animal, every Dog automatically has a name field and can eat() and sleep() — you didn't rewrite any of that. Call myDog.eat() and it runs the exact code defined up in Animal. That's inheritance: the child reuses the parent's members as if they were its own.

animal.ts
class Animal {
  constructor(public name: string) {}

  eat() {
    return this.name + " is eating";
  }

  makeSound() {
    return "(some generic animal noise)";
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);          // run Animal's constructor first
  }

  // OWN method — only dogs have this
  fetch() {
    return this.name + " fetches the ball";
  }

  // OVERRIDE — replaces Animal's makeSound for dogs
  makeSound() {
    return "Woof!";
  }
}

const d = new Dog("Rex");
d.eat();        // inherited from Animal → "Rex is eating"
d.makeSound();  // overridden by Dog     → "Woof!"
d.fetch();      // Dog's own method      → "Rex fetches the ball"

Calling the parent with `super`

When a subclass has its own constructor, it must first set up the part of the object that the parent owns. super(name) calls the parent's constructor so the inherited name field gets initialized before the child adds anything. You can also use super.someMethod() inside an override to reuse the parent's version and add to it, rather than replacing it entirely.

Overriding vs. inheriting

If a subclass defines a method with the same name as one in the parent, the subclass's version overrides it. After that, calling the method on a Dog runs the Dog version — "Woof!" — not the generic Animal one. Methods the subclass doesn't redefine are simply inherited and run the parent's code unchanged. So a Dog inherits eat() and sleep() but overrides makeSound().

The "is-a" test

Inheritance models an is-a relationship. Before writing B extends A, say it out loud: "a Dog is an Animal" — true, so inheritance fits. "a Car is an Engine"? No — a car has an engine. That's a different relationship, and inheritance would be the wrong tool there.

Single vs. multiple inheritance

Some languages let a class inherit from only one parent — Java and C# are single-inheritance (you then mix in extra capabilities through interfaces). Others, like C++ and Python, allow multiple inheritance, where a class can extend several parents at once. Multiple inheritance is powerful but can get confusing when two parents define the same method, so single inheritance plus interfaces is the more common default.

Prefer composition when it isn't truly "is-a"

Inheritance is tempting as a shortcut for code reuse, but reach for it only when the is-a test genuinely holds. If a type merely needs some behavior — but isn't really a kind of the parent — prefer composition: hold the other object as a field and delegate to it. We'll dig into composition over inheritance in a later lesson; for now, just remember that not every "I want to reuse this" is an inheritance.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

At the top sits the base class Animal with its members. Below it, Dog, Cat, and Bird branch off — each one inherits everything from Animal, adds its own method, and overrides makeSound(). Click a subclass, then call its methods and watch the console say exactly where each method came from.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Pick a subclass and read its members

Click Dog, then Cat, then Bird in the family tree. Watch the instance card relabel every member: things tagged inherited came straight from Animal, own is the method that subclass added (like fetch()), and overridden is makeSound(), which each species redefines.

try 02

Call an inherited method

With any subclass selected, press eat() or sleep(). The console narrates → inherited from Animal and the highlight in the tree jumps up to Animal, proving the code that runs lives in the parent — the subclass didn't rewrite it.

try 03

See an override resolve

Press makeSound() and notice the console says Dog overrides → "Woof!" — the highlight stays on the subclass, not Animal. Then press the subclass-only button (fetch() for Dog, scratch() for Cat, fly() for Bird); it's available only while that subclass is selected, because it's its own method.

In practice

When to use it — and what trips people up

When inheritance is the right tool

  • When the relationship is a genuine is-a — a Dog is an Animal, a SavingsAccount is a BankAccount. Say it aloud; if it sounds wrong, it probably is.
  • When several types share real, common behavior that you want to define once in a parent and reuse everywhere.
  • When you want polymorphism — treating different subclasses uniformly through the parent type (loop over a list of Animal and call makeSound() on each).

When to prefer composition instead

If you're inheriting just to grab some handy code — but the child isn't truly a kind of the parent — stop. Use composition: hold the helper as a field and call it. Composition is more flexible, avoids fragile hierarchies, and doesn't lie about the relationship. Inheritance is for is-a; composition is for has-a.

What it gives you

  • Reuse — write shared fields and methods once in the parent, and every subclass gets them for free.
  • Enables polymorphism — code can work against the base type and let each subclass supply its own behavior.
  • Models real hierarchies cleanly when an honest is-a relationship exists.
  • Centralizes change — fix or extend the parent and every subclass benefits at once.

Common mistakes

  • Deep, sprawling hierarchies become fragile — a small change in a base class can ripple into every descendant.
  • Inheriting purely for code reuse (when it isn't really is-a) couples unrelated types and invites composition's job onto the wrong tool.
  • A careless override can break the parent's contract (the Liskov Substitution Principle), so subclasses no longer behave as the base promised.
  • Subclasses are tightly coupled to their parent's internals, making the base class hard to change without breaking children.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

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

  eat() {
    return this.name + " is eating";
  }

  makeSound() {
    return "(generic animal noise)";
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name); // initialize the inherited 'name' field
  }

  // own method — added by Dog
  fetch() {
    return this.name + " fetches the ball";
  }

  // override — Dog's own version of makeSound
  makeSound() {
    return "Woof!";
  }
}

const rex = new Dog("Rex");
rex.eat();        // inherited → "Rex is eating"
rex.makeSound();  // overridden → "Woof!"
rex.fetch();      // own        → "Rex fetches the ball"

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Nothing is scored or saved.

question 01 / 04

What does it mean for Dog to extend Animal?

question 02 / 04

Inside Dog's constructor you write super(name). What is it for?

question 03 / 04

Dog defines its own makeSound() returning "Woof!", but does not redefine eat(). What runs for each call on a Dog?

question 04 / 04

You want a Car to reuse some logic from an Engine class. Should Car extends Engine?

0/4 answered