Inheritance
Inheritance lets one class build on another, reusing its fields and methods while adding or specializing behavior. In TypeScript you create a subclass with the extends keyword, reach into the base class with super, and share members down the hierarchy using the protected modifier. TypeScript adds full static checking on top of JavaScript’s prototype-based inheritance: the compiler verifies constructor calls, enforces that overrides remain type-compatible, and tracks which members each level can see. This page covers extending classes, the rules around super, protected members, and how subtype relationships flow through the type system.
Extending a class
A subclass declared with extends inherits every accessible member of its base class. It can use those members as if they were its own and add new ones.
class Animal {
constructor(public name: string) {}
move(distance: number): string {
return `${this.name} moved ${distance}m`;
}
}
class Dog extends Animal {
bark(): string {
return `${this.name} says woof`;
}
}
const d = new Dog("Rex");
d.move(10); // ✅ inherited
d.bark(); // ✅ own method
Dog gains name and move from Animal for free, then layers on bark. A class can extend only one base class — TypeScript, like JavaScript, has single inheritance.
Calling super in the constructor
When a subclass declares its own constructor, it must call super(...) before using this. The super call runs the base constructor so inherited fields are initialized first.
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // ✅ must run before touching `this`
this.breed = breed;
}
}
// ❌ Error: 'super' must be called before accessing 'this'
class Cat extends Animal {
constructor() {
this.name = "Felix"; // used before super()
super("Felix");
}
}
If a subclass omits its constructor entirely, TypeScript synthesizes one that forwards all arguments to super. You only write a constructor when you need extra parameters or setup.
Calling super methods
super.method() invokes the base class implementation of a method, even when the subclass overrides it. This lets you extend behavior rather than replace it.
class Logger {
log(message: string): string {
return `[LOG] ${message}`;
}
}
class TimestampLogger extends Logger {
log(message: string): string {
const base = super.log(message); // call parent version
return `${new Date().toISOString()} ${base}`;
}
}
new TimestampLogger().log("ready"); // "2026-06-29T... [LOG] ready"
super.log(message) runs Logger.log and returns its result, which the override then decorates. Without super, you would have to duplicate the base logic.
Protected members in a hierarchy
A protected member is visible inside the declaring class and all its subclasses, but hidden from outside code. It is the standard way to share state and helpers down a hierarchy without exposing them publicly.
class Vehicle {
protected speed = 0;
protected accelerate(by: number): void {
this.speed += by;
}
}
class Car extends Vehicle {
drive(): number {
this.accelerate(30); // ✅ subclass can use protected helper
return this.speed; // ✅ subclass can read protected field
}
}
new Car().drive(); // 30
new Car().speed; // ❌ Error: Property 'speed' is protected
Car freely uses speed and accelerate because it extends Vehicle, but external callers cannot. Contrast this with private, which even subclasses cannot access.
Subtype substitutability
A subclass instance is assignable wherever the base type is expected — this is the Liskov substitution principle expressed in the type system. It is what makes polymorphism work.
class Shape {
area(): number {
return 0;
}
}
class Circle extends Shape {
constructor(private r: number) {
super();
}
area(): number {
return Math.PI * this.r ** 2;
}
}
const shapes: Shape[] = [new Shape(), new Circle(5)]; // ✅ Circle is a Shape
const total = shapes.reduce((sum, s) => sum + s.area(), 0);
| Modifier | Same class | Subclass | Outside code |
|---|---|---|---|
public | Yes | Yes | Yes |
protected | Yes | Yes | No |
private | Yes | No | No |
Deep inheritance chains become brittle and hard to reason about. Before reaching for a third or fourth level of
extends, consider composition — building behavior from smaller collaborating objects — which often scales better.
Best Practices
- Always call
super(...)first in a subclass constructor before touchingthis. - Use
protectedfor state subclasses need; reserveprivatefor purely internal details. - Call
super.method()to extend base behavior instead of duplicating it. - Keep hierarchies shallow; prefer composition over deep inheritance trees.
- Let TypeScript synthesize the constructor when the subclass adds no new parameters.
- Ensure overrides stay type-compatible so subtypes remain substitutable for their base.
- Mark intentional overrides with the
overridekeyword for clearer, safer code.