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
extendsa 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
overridekeyword 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.
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";
}
}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).
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.
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
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 theif, TypeScript narrowspetfromAnimaltoDog. - 2
Make the compatible change
In the compatibility example change
Square.areatoarea(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
Fix the constructor order
Copy the Kid snippet into a live example, move
this.nick = "y";belowsuper(id);and check the Types panel is clean. Then addconsole.log(new Kid(1).nick, new Kid(1).id)and predict the output (y 1).
Code & diagrams
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
truegreet 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
hiThis 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
hiSquare 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
3There 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=derivedBreak 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.
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 understrictFunctionTypes. 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 becausestringandnumberare unrelated. - ▸
useDefineForClassFields(default on fortargetES2022+) 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.memberonly works for methods, not fields: fields live on the instance, not on the parent's prototype, so TypeScript rejectssuper.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:
Dogmay take different constructor parameters fromAnimal. Substitutability is checked for instances, not for how they were built.
Remember this
- 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 AnimalmakesDoga subclass (also: child class, derived class) ofAnimal, the superclass (parent, base class). ADoggets every field and method ofAnimal. This is called inheritance. It models an "is a" relationship: a dog *is an* animal, so wherever anAnimalis expected, aDogis accepted. That is substitutability, and it is whyconst pet: Animal = new Dog(...)is legal. - 2
A subclass constructor must call **
super(...)** to run the parent's constructor first, passing what the parent needs. Untilsuperhas run, the new object does not exist yet, so touchingthisbefore 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
The declared type decides what you can call. In
const pet: Animal = new Dog(...)the object really is aDogandpet.speak()runs the dog's version at runtime (polymorphism: the same call does different things for different classes), butpet.fetch()is an error because the *type*Animalhas nofetch. To usefetchyou must narrow, for example withinstanceof Dog(Topic 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
stringwhere the parent takes anumberbreaks callers that only know the parent, and the error isTS2416with the mismatch spelled out. - 5
The **
override** keyword (TypeScript 4.3) says "I mean to replace a parent member". If the parent has no such member, TypeScript reportsTS4113immediately. This catches the quiet bug: someone renamesgreettowelcomein the parent, and the child'sgreetsilently becomes an unused, separate method. The option **noImplicitOverride** goes further and makesoverriderequired whenever you replace a parent member (TS4114). It is not part ofstrict; many teams switch it on. - 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 (orundefined), not the child's (the last live example shows it). Avoid calling overridable methods from a constructor. - 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
What does extends give a class, and what must a derived constructor do first?
Why is pet.fetch() an error when pet: Animal = new Dog(...), and how do you fix it?
What does the override keyword do, and what does noImplicitOverride add?
What rule must an overriding method follow, and why?
Practice
Write Vehicle with a wheels field and describe() returning text. Write Bike extends Vehicle that overrides describe() using super.describe() and override.
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.
- ↔
noImplicitOverrideadds 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 usessuper.method().I can explain why the declared type, not the runtime class, decides what I can call.
I can use
overrideand say whatTS4113andTS4114mean.I can state the compatibility rule for overridden methods.
I can explain why a parent constructor may see the parent's field value.