Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~21 min· topic 3 of 8

Topic 6.3

Inheritance and override

In one line

A subclass extends a parent class and gets its fields and methods for free, then adds or replaces some. TypeScript checks that every replacement still fits the parent's promise, and the override keyword makes replacing explicit.

Think of it like this

A family recipe book. The grandmother's pancake recipe is the parent. Your version says "make grandmother's pancakes, but add blueberries". You do not copy the whole recipe; you point at the original and write only the change. If grandmother later improves her batter, your pancakes improve too. A subclass is your version of the recipe: it extends a parent class and adds or changes only what is different.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Inheritance
A class getting the fields and methods of another class.
Superclass / base class
The parent class being extended.
Subclass / derived class
The child class that extends a parent.
super
In a constructor, super(...) calls the parent constructor. In a method, super.name() calls the parent's version.
Override
Replacing a parent's method in the child with a new version of the same name.
Polymorphism
The same method call doing different things depending on which class the object really is.
Substitutability
A subclass instance can be used anywhere the parent type is expected.
noImplicitOverride
A compiler option that makes the override keyword required when replacing a parent member.
Composition
Building behaviour by holding other objects instead of extending a class.

Step by step

01extends and super

Dog extends Animal inherits name and speak. Dog adds its own field, tricks, and its own method, fetch.

The constructor passes name up with super(name). The method speak reuses the parent's text with super.speak() and adds to it.

animals.tswhole filets
class Animal {
  constructor(public name: string) {}
  speak() {
    return this.name + " makes a sound";
  }
}

class Dog extends Animal {
  constructor(name: string, public tricks: string[]) {
    super(name);
  }
  speak() {
    return super.speak() + ": woof";
  }
  fetch() {
    return this.name + " fetches";
  }
}
extends and superdiagram
Rendering diagram…

02The type decides what you may call

const pet: Animal = new Dog("Rex", ["sit"]) is fine, a dog is an animal. pet.speak() runs the dog's version (polymorphism).

pet.fetch() is an error: the variable's *type* is Animal, and Animal has no fetch. Narrow first with pet instanceof Dog.

03override says what you mean

Write override before a member that replaces a parent member. If you misspell it or the parent removes it, TypeScript tells you (TS4113). Turn on noImplicitOverride and forgetting the word is also an error (TS4114).

override.tswhole filets
class Base {
  greet() { return "hello"; }
}
class Child extends Base {
  override greet() { return "hi"; }  // ok
  override wave() { return "wave"; } // error TS4113: not declared in the base class
}

04A replacement must fit the parent's promise

The parent says area(scale: number): number. A child area(scale: string) cannot stand in for it, because a caller holding a Shape would pass a number. TypeScript compares the two function types and prints the mismatch as three indented lines under the error, each going one level deeper.

A child area() that takes fewer parameters is allowed: it simply ignores what the caller passes.

05Constructors run parent first

The child's constructor calls super(...) before it may use this. Writing this.nick = "y" first is TS17009.

Field initialisers of the child run right after super() returns, which is why a parent constructor that reads a redeclared field sees the old value.

kid.tswhole filets
class Parent {
  constructor(public id: number) {}
}
class Kid extends Parent {
  nick = "x";
  constructor(id: number) {
    this.nick = "y"; // error TS17009: 'super' must be called before accessing 'this'
    super(id);
  }
}

Try it yourself

  1. 1

    Narrow to call fetch

    In the first example replace the last line with if (pet instanceof Dog) { console.log(pet.fetch()); }. Predict the Types panel.

    It becomes clean and prints Rex fetches: inside the if, TypeScript narrows pet from Animal to Dog.

  2. 2

    Make the compatible change

    In the compatibility example change Square.area to area(scale: number): number { return scale * scale; }. Predict, then check. Then try adding a second parameter: area(scale: number, extra: number).

    The first change is clean. The second is an error again: the parent can be called with one argument, and the child now demands two.

  3. 3

    Fix the constructor order

    Copy the Kid snippet into a live example, move this.nick = "y"; below super(id); and check the Types panel is clean. Then add console.log(new Kid(1).nick, new Kid(1).id) and predict the output (y 1).

Code & diagrams

extends, super and polymorphism

The runtime picks the dog's speak() even though the variable is typed Animal. The last line is an error because Animal has no fetch; narrow with instanceof Dog to call it.

Console

(nothing was printed)

Expected console output

Rex makes a sound: woof
true
override catches a member that is not in the parent TypeScript 4.3+

greet is a real override, wave is not. The error lists the base class by name. Delete the word override from wave and the error disappears: the checker then treats it as a new method.

Console

(nothing was printed)

Expected console output

hi
noImplicitOverride makes the keyword required TypeScript 4.3+

This example turns noImplicitOverride on. Without the option the same code is clean. Add the word override before greet to fix it.

Console

(nothing was printed)

Expected console output

hi
A replacement must be compatible with the parent

Square wants a string where Shape promised a number, so it is rejected, and the error spells out which parameter clashes. Circle takes fewer parameters, which is fine.

Console

(nothing was printed)

Expected console output

3
The parent constructor runs before the child's fields

There is no type error, yet the first line shows the parent's value. The child's name = "derived" is assigned only after super() has finished. Never call overridable methods from a constructor.

Console

(nothing was printed)

Expected console output

Base sees: name=base
after: name=derived

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Touch this before super

Use the Kid snippet: put this.nick = "y"; above the super(id); call in the constructor.

terminal
$ Types panel
── what you'll see ──
index.ts(7,5): error TS17009: 'super' must be called before accessing 'this' in the constructor of a derived class.

Myth vs fact

Myth

A subclass can change a method to take any parameters it likes.

Fact

It must stay compatible with the parent's signature so a parent-typed variable can still call it safely. Fewer parameters are fine, incompatible ones are TS2416.

Myth

override changes how the code runs.

Fact

It is erased. It only asks the checker to confirm that the parent really has that member.

Myth

More inheritance levels means better design.

Fact

Deep hierarchies couple children to every ancestor's internals. A shallow hierarchy plus composition is easier to change.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Method parameters are checked bivariantly by default for methods (the method shorthand area(): void), but contravariantly for function-typed properties under strictFunctionTypes. That looseness is deliberate (Topic 7.7 covers variance, Topic 16.5 the soundness holes), and it is why an override whose parameter is a wider or narrower related type can be accepted. Square.area(scale: string) fails here only because string and number are unrelated.

  • ▸

    useDefineForClassFields (default on for target ES2022+) is what makes a redeclared child field overwrite the parent's value after the parent constructor. The same order applies with the older assignment semantics, so the rule holds either way: never rely on a child's field value inside a parent constructor.

  • ▸

    super.member only works for methods, not fields: fields live on the instance, not on the parent's prototype, so TypeScript rejects super.name (TS2855: Class field 'name' defined by the parent class is not accessible in the child class via super.).

  • ▸

    A constructor is not inherited by signature checks: Dog may take different constructor parameters from Animal. Substitutability is checked for instances, not for how they were built.

Remember this

  1. 1

    The problem this solves: Two similar classes often share most of their code, and copying it means fixing each copy when something changes. Inheritance reuses the shared part. class Dog extends Animal makes Dog a subclass (also: child class, derived class) of Animal, the superclass (parent, base class). A Dog gets every field and method of Animal. This is called inheritance. It models an "is a" relationship: a dog *is an* animal, so wherever an Animal is expected, a Dog is accepted. That is substitutability, and it is why const pet: Animal = new Dog(...) is legal.

  2. 2

    A subclass constructor must call **super(...)** to run the parent's constructor first, passing what the parent needs. Until super has run, the new object does not exist yet, so touching this before it is an error (TS17009: 'super' must be called before accessing 'this' in the constructor of a derived class). Inside methods, super.speak() calls the parent's version of a method, which is how a child extends behaviour instead of throwing the parent's away.

  3. 3

    The declared type decides what you can call. In const pet: Animal = new Dog(...) the object really is a Dog and pet.speak() runs the dog's version at runtime (polymorphism: the same call does different things for different classes), but pet.fetch() is an error because the *type* Animal has no fetch. To use fetch you must narrow, for example with instanceof Dog (Topic 4.4).

  4. 4

    A method in a subclass overrides the parent's when it has the same name. TypeScript demands that the new version is compatible: anywhere the parent's method could be called, the child's must work too. Concretely, parameters may be the same or fewer (a child that ignores an argument is fine) and the return type must be the same or more specific. A child that wants a string where the parent takes a number breaks callers that only know the parent, and the error is TS2416 with the mismatch spelled out.

  5. 5

    The **override** keyword (TypeScript 4.3) says "I mean to replace a parent member". If the parent has no such member, TypeScript reports TS4113 immediately. This catches the quiet bug: someone renames greet to welcome in the parent, and the child's greet silently becomes an unused, separate method. The option **noImplicitOverride** goes further and makes override required whenever you replace a parent member (TS4114). It is not part of strict; many teams switch it on.

  6. 6

    Order of events matters. When you write new Derived(), the parent's constructor runs first, then the child's field initialisers, then the rest of the child's constructor. So if the parent constructor calls a method that reads a field the child redeclares, it sees the parent's value (or undefined), not the child's (the last live example shows it). Avoid calling overridable methods from a constructor.

  7. 7

    Inheritance is the strongest coupling in object-oriented code: the child depends on the parent's internals, and changing the parent can break every child. The rule of thumb is prefer composition (an object that *has* another object and calls it) unless there is a true "is a" relationship and the parent was designed to be extended.

Explain it without notes

01

What does extends give a class, and what must a derived constructor do first?

02

Why is pet.fetch() an error when pet: Animal = new Dog(...), and how do you fix it?

03

What does the override keyword do, and what does noImplicitOverride add?

04

What rule must an overriding method follow, and why?

Practice

01

Write Vehicle with a wheels field and describe() returning text. Write Bike extends Vehicle that overrides describe() using super.describe() and override.

02

A parent has render(): string. Write a child whose render returns a number, read the error, and fix it.

Trade-offs

  • ↔

    Inheritance reuses code in one line but couples the child to the parent forever. Composition needs a few more lines of forwarding but lets you swap parts without touching a hierarchy.

  • ↔

    noImplicitOverride adds a word to every override and prevents a class of silent breakage after refactors.

Done when you can

  • I can write a subclass that calls super(...) and uses super.method().

  • I can explain why the declared type, not the runtime class, decides what I can call.

  • I can use override and say what TS4113 and TS4114 mean.

  • I can state the compatibility rule for overridden methods.

  • I can explain why a parent constructor may see the parent's field value.