Inheritance & super
Inheritance allows a class to reuse and extend the behaviour of another class. TypeScript's class inheritance is built on JavaScript's prototype chain, with the added safety of static type checking. The super keyword is the bridge between a derived class and its parent.
Basic Inheritance with extends
A class that extends another inherits all of its public and protected members. The derived class can add new members, override existing ones, or simply reuse the parent's behaviour as-is.
class Animal {
constructor(public name: string) {}
move(distance = 0): void {
console.log(`${this.name} moved ${distance}m.`);
}
toString(): string {
return `Animal(${this.name})`;
}
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name); // ← must call super before using 'this'
this.breed = breed;
}
bark(): void {
console.log('Woof!');
}
// Inherited: move(), toString(), name property
}
const dog = new Dog('Rex', 'Labrador');
dog.bark(); // Woof!
dog.move(10); // Rex moved 10m.
console.log(dog.name); // Rex
console.log(dog.breed); // Labradorsuper() before accessing this. TypeScript enforces this — you will get a compile error if you forget.The super Keyword
super has two uses in TypeScript:
super(...args)— calls the parent constructor (required in derived class constructors)super.method()— calls the parent's version of an overridden method
class Vehicle {
constructor(
public make: string,
public model: string,
public year: number,
) {}
describe(): string {
return `${this.year} ${this.make} ${this.model}`;
}
start(): void {
console.log('Engine started.');
}
}
class ElectricVehicle extends Vehicle {
constructor(
make: string,
model: string,
year: number,
public batteryCapacityKwh: number,
) {
super(make, model, year); // ← calls Vehicle's constructor
}
// Override describe() — but augment rather than replace
describe(): string {
const base = super.describe(); // ← calls Vehicle's describe()
return `${base} (Electric, ${this.batteryCapacityKwh}kWh)`;
}
// Override start() completely
start(): void {
// Do NOT call super.start() — different behaviour
console.log('Electric motor engaged silently.');
}
}
const tesla = new ElectricVehicle('Tesla', 'Model 3', 2024, 82);
console.log(tesla.describe()); // 2024 Tesla Model 3 (Electric, 82kWh)
tesla.start(); // Electric motor engaged silently.Method Overriding
A derived class can override any public or protected method from the parent. TypeScript checks that the override signature is compatible. If noImplicitOverride is enabled (recommended), you must use the override keyword explicitly.
class Shape {
area(): number {
return 0;
}
perimeter(): number {
return 0;
}
toString(): string {
return `Shape(area=${this.area().toFixed(2)})`;
}
}
class Circle extends Shape {
constructor(public radius: number) {
super();
}
override area(): number {
return Math.PI * this.radius ** 2;
}
override perimeter(): number {
return 2 * Math.PI * this.radius;
}
override toString(): string {
return `Circle(r=${this.radius}, area=${this.area().toFixed(2)})`;
}
}
class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super();
}
override area(): number {
return this.width * this.height;
}
override perimeter(): number {
return 2 * (this.width + this.height);
}
}
const shapes: Shape[] = [new Circle(5), new Rectangle(4, 6)];
shapes.forEach(s => console.log(s.area().toFixed(2)));
// 78.54
// 24.00override keyword is optional unless you enable noImplicitOverride in tsconfig. With that flag on, TypeScript will error if you override a method without writing override — preventing accidental overrides when base class methods are renamed.Access Modifiers in Inheritance
Modifier | Same class | Derived class | Outside class |
|---|---|---|---|
public | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ❌ |
private | ✅ | ❌ | ❌ |
class BankAccount {
public readonly id: string;
protected balance: number;
private transactionLog: string[] = [];
constructor(id: string, initialBalance: number) {
this.id = id;
this.balance = initialBalance;
}
protected recordTransaction(note: string) {
this.transactionLog.push(`${new Date().toISOString()}: ${note}`);
}
getBalance(): number {
return this.balance;
}
}
class SavingsAccount extends BankAccount {
private interestRate: number;
constructor(id: string, initialBalance: number, interestRate: number) {
super(id, initialBalance);
this.interestRate = interestRate;
}
deposit(amount: number): void {
this.balance += amount; // ✅ protected — accessible
this.recordTransaction(`Deposit: ${amount}`); // ✅ protected method
// ❌ Error: Property 'transactionLog' is private in type 'BankAccount'
// this.transactionLog.push('...');
}
applyInterest(): void {
const interest = this.balance * this.interestRate;
this.balance += interest; // ✅ protected
this.recordTransaction(`Interest: ${interest.toFixed(2)}`);
}
}
const account = new SavingsAccount('SA001', 1000, 0.05);
account.deposit(500);
account.applyInterest();
console.log(account.getBalance()); // 1575super in Property Accessors
super also works with getters and setters — you can call the parent's getter inside a derived getter.
class Product {
constructor(
public name: string,
protected basePrice: number,
) {}
get price(): number {
return this.basePrice;
}
get displayName(): string {
return this.name;
}
}
class DiscountedProduct extends Product {
constructor(
name: string,
basePrice: number,
private discountPercent: number,
) {
super(name, basePrice);
}
override get price(): number {
return super.price * (1 - this.discountPercent / 100);
}
override get displayName(): string {
return `${super.displayName} (-${this.discountPercent}%)`;
}
}
const item = new DiscountedProduct('Widget', 100, 20);
console.log(item.price); // 80
console.log(item.displayName); // Widget (-20%)Abstract Classes
An abstract class is a class that cannot be instantiated directly — it exists to be extended. Abstract methods must be implemented by derived classes.
Abstract classes occupy the middle ground between interfaces (no implementation) and concrete classes (full implementation).
abstract class Renderer {
// Abstract method — no implementation here, subclass must provide it
abstract render(template: string, context: Record<string, unknown>): string;
// Concrete method — shared by all subclasses
renderToFile(
path: string,
template: string,
context: Record<string, unknown>,
): void {
const output = this.render(template, context); // calls the abstract method
console.log(`Writing ${output.length} chars to ${path}`);
// fs.writeFileSync(path, output);
}
// Template method pattern — defines the algorithm, delegates steps
renderPage(templateName: string, data: Record<string, unknown>): string {
const template = this.loadTemplate(templateName);
const content = this.render(template, data);
return this.wrapInLayout(content);
}
protected loadTemplate(name: string): string {
return `<template>${name}</template>`; // base implementation
}
private wrapInLayout(content: string): string {
return `<html><body>${content}</body></html>`;
}
}
// ❌ Error: Cannot create an instance of an abstract class
// new Renderer();
class HandlebarsRenderer extends Renderer {
render(template: string, context: Record<string, unknown>): string {
// Real implementation would use Handlebars.compile()
return template.replace(/\{\{(\w+)\}\}/g, (_, key) =>
String(context[key] ?? ''),
);
}
}
class MustacheRenderer extends Renderer {
render(template: string, context: Record<string, unknown>): string {
return template.replace(/\{\{(\w+)\}\}/g, (_, key) =>
String(context[key] ?? ''),
);
}
protected override loadTemplate(name: string): string {
return `<!-- Mustache: ${name} -->`;
}
}
const r = new HandlebarsRenderer();
r.renderToFile('index.html', 'Hello {{name}}!', { name: 'World' });Multi-Level Inheritance
Inheritance chains can span multiple levels. Each level can override methods and call super to reach any ancestor in the chain.
class Base {
greet(): string { return 'Hello from Base'; }
}
class Middle extends Base {
override greet(): string {
return `${super.greet()} + Middle`;
}
}
class Leaf extends Middle {
override greet(): string {
return `${super.greet()} + Leaf`;
}
}
const l = new Leaf();
console.log(l.greet());
// "Hello from Base + Middle + Leaf"instanceof and Type Narrowing
TypeScript narrows types in branches where instanceof checks succeed. This is especially useful with inheritance hierarchies.
class HTTPError extends Error {
constructor(
public statusCode: number,
message: string,
) {
super(message);
this.name = 'HTTPError';
}
}
class NetworkError extends Error {
constructor(
public readonly originalError: Error,
message = 'Network request failed',
) {
super(message);
this.name = 'NetworkError';
}
}
class TimeoutError extends NetworkError {
constructor(public timeoutMs: number) {
super(new Error('timeout'), `Request timed out after ${timeoutMs}ms`);
this.name = 'TimeoutError';
}
}
function handleError(err: unknown): string {
if (err instanceof TimeoutError) {
// err: TimeoutError — access .timeoutMs
return `Timeout after ${err.timeoutMs}ms — try again`;
}
if (err instanceof HTTPError) {
// err: HTTPError — access .statusCode
if (err.statusCode === 401) return 'Please log in again';
if (err.statusCode === 404) return 'Resource not found';
return `HTTP ${err.statusCode}: ${err.message}`;
}
if (err instanceof Error) {
return `Error: ${err.message}`;
}
return 'An unknown error occurred';
}instanceof checks from most specific to most general — TimeoutError before NetworkError before Error.Composition vs Inheritance
Inheritance is powerful but has limits. Favour composition (combining behaviour through interfaces and member properties) over deep inheritance chains.
// Inheritance approach — becomes rigid with multiple "flavours"
class LoggedUserRepository extends UserRepository {
override async findById(id: string) {
console.log(`findById: ${id}`);
return super.findById(id);
}
}
class CachedUserRepository extends UserRepository { /* ... */ }
class CachedAndLoggedUserRepository extends ??? { /* impossible cleanly */ }
// Composition approach — mix and match behaviours
interface UserRepo {
findById(id: string): Promise<User | null>;
}
class LoggingDecorator implements UserRepo {
constructor(private inner: UserRepo) {}
async findById(id: string) {
console.log(`findById: ${id}`);
return this.inner.findById(id);
}
}
class CachingDecorator implements UserRepo {
private cache = new Map<string, User>();
constructor(private inner: UserRepo) {}
async findById(id: string) {
if (this.cache.has(id)) return this.cache.get(id)!;
const user = await this.inner.findById(id);
if (user) this.cache.set(id, user);
return user;
}
}
// Compose freely — order determines behaviour
const repo = new CachingDecorator(new LoggingDecorator(new DbUserRepository()));UserRepository class is closed for modification but open for extension via decoration.Summary
extends creates an inheritance relationship — the derived class gets all public and protected members.
super() must be called before accessing this in a derived constructor.
super.method() calls the parent class's version of an overridden method.
protected members are accessible in derived classes but not outside the hierarchy.
private members are not inherited — only the declaring class can access them.
The override keyword (required with noImplicitOverride) makes override intent explicit and safe.
Abstract classes define a partial implementation that subclasses must complete.
instanceof narrows types in conditional branches — order checks from most specific to most general.
Prefer composition over deep inheritance hierarchies for complex behaviour.