Inheritance

Think about how we describe living things. A dog and a cat are obviously different — one barks, one meows — but they also share a huge amount: both are animals, both have a name, both eat, both sleep, both can be described. It would be silly to write out "has a name", "eats", "sleeps" separately for every kind of animal. We'd rather say it once, for animals in general, and then let each specific animal add only what makes it special.

Inheritance is exactly this idea in code. You write a general superclass (also called the parent or base class), and then a more specific subclass (the child or derived class) extends it. The subclass automatically inherits all the fields and methods of its parent — the shared behaviour is written just once — and is then free to add new members of its own and to override inherited ones to behave differently.

Inheritance models an "is-a" relationship: a Dog is-an Animal, a Cat is-an Animal. If you can honestly say "a B is an A", then making B extends A is a good fit — and that little test will save you from the most common design mistake, which we'll meet at the end.

A picture: the class hierarchy

We draw inheritance as a tree. The general class sits at the top; each arrow runs from a subclass up to its superclass, meaning "is-a". Everything an Animal can do, a Dog and a Cat can do too — because they are animals — plus their own extras.

Notice the shared members — name, eat(), describe() — live only at the top. They are not copied down; they are inherited. Change describe() once in Animal and every subclass instantly gets the new behaviour. This is the whole payoff: write shared behaviour once.

Your first hierarchy: extend and override

Here is Animal, a base class with a name field and two methods. Then Dog and Cat each extends Animal. They don't repeat name or describe() — they get those for free. What they do is override speak() so each makes its own sound. Press Run.

class Animal { constructor(public name: string) {} speak(): string { return "Some generic animal sound"; } describe(): string { return this.name + " says: " + this.speak(); } } class Dog extends Animal { speak(): string { // override: a Dog barks return "Woof!"; } } class Cat extends Animal { speak(): string { // override: a Cat meows return "Meow!"; } } const rex = new Dog("Rex"); const felix = new Cat("Felix"); console.log(rex.describe()); // Rex says: Woof! console.log(felix.describe()); // Felix says: Meow!

Look closely at describe(). It lives only in Animal, yet when rex.describe() runs, the call to this.speak() uses the Dog's version. The object always runs its own most specific method — a Dog barks even though describe() was written for a generic animal. That's the power of overriding.

Adding new members, and calling super

A subclass isn't limited to the parent's members — it can add its own. A Dog can fetch(); an Animal in general cannot. And when a subclass needs its own constructor (say, to store an extra field like breed), it must first call the parent's constructor with super(...) so the inherited part of the object is set up. You can also call super.method() to reuse the parent's version of a method and then add to it. Run this:

class Animal { constructor(public name: string) {} speak(): string { return "..."; } describe(): string { return this.name + " says: " + this.speak(); } } class Dog extends Animal { constructor(name: string, public breed: string) { super(name); // set up the inherited Animal part first } speak(): string { return "Woof!"; } fetch(): string { // a brand-new method Animal doesn't have return this.name + " fetches the ball!"; } describe(): string { // reuse the parent's describe, then add the breed return super.describe() + " (a " + this.breed + ")"; } } const rex = new Dog("Rex", "Labrador"); console.log(rex.describe()); // Rex says: Woof! (a Labrador) console.log(rex.fetch()); // Rex fetches the ball!

Three things are happening here: super(name) initialises the inherited name; fetch() is a new member on Dog only; and super.describe() lets Dog's describe() build on the parent's rather than replacing it entirely.

If a variable's type is Animal, the compiler only lets you call things every animal has — so animal.fetch() is a type error, because not every animal can fetch. The object at runtime really is a Dog and does have fetch(), but from the wider Animal viewpoint that extra ability is invisible until you narrow the type back down (e.g. with a check like if (a instanceof Dog)). A subclass can always be used where the parent is expected; the reverse is not guaranteed.

Why it pays off: treat them all through the base

Because every subclass is-an Animal, we can put a Dog, a Cat and anything else that extends Animal into a single Animal[] and process them uniformly. We call describe() on each without caring which specific kind it is — yet each still speaks with its own voice. This is the pattern that makes big programs manageable:

class Animal { constructor(public name: string) {} speak(): string { return "..."; } describe(): string { return this.name + " says " + this.speak(); } } class Dog extends Animal { speak(): string { return "Woof!"; } } class Cat extends Animal { speak(): string { return "Meow!"; } } class Cow extends Animal { speak(): string { return "Moo!"; } } // One list, mixed types — all are Animals. const farm: Animal[] = [new Dog("Rex"), new Cat("Felix"), new Cow("Daisy")]; for (const a of farm) { console.log(a.describe()); // each runs its OWN speak() }

Adding a new animal later — a Sheep that says "Baa!" — needs no change to this loop at all. Write the subclass, drop it in the list, done. Shared code stays shared; differences stay local to each subclass.

The single most common inheritance mistake is using it for a "has-a" relationship. Ask the little question before you write extends:

Storing a part as a field like this is called composition. The reliable test: if you can say "a B is a kind of A", use inheritance; if you'd say "an A has a B" or "is made of a B", use a field instead.

class Engine { start(): string { return "Vroom"; } } // ✓ is-a: Car extends Vehicle class Vehicle { constructor(public wheels: number) {} } class Car extends Vehicle { // ✗ NOT "class Car extends Engine" — a car is not an engine! // ✓ has-a: the engine is a FIELD (composition) engine = new Engine(); constructor() { super(4); } }

Reaching for extends whenever two classes share a little code — rather than only when there's a genuine "is-a" — leads to tangled, fragile hierarchies. When in doubt, prefer composition: it's more flexible and easier to change.