Inheritance & Abstract Classes

A class can extend another class to reuse and specialise its behaviour. TypeScript adds checks on top of JavaScript's inheritance: protected members for subclasses, the override keyword to catch mistakes, and abstract classes that define a template other classes must complete.

Extending a class

TS extends.ts
class Animal {
  constructor(public name: string) {}

  move(distance: number) {
    return `${this.name} moved ${distance}m`;
  }
}

class Bird extends Animal {
  fly(height: number) {
    return `${this.name} flew to ${height}m`;
  }
}

const robin = new Bird("Robin");
console.log(robin.move(2));
console.log(robin.fly(30));
Output
Robin moved 2m
Robin flew to 30m

Bird inherits everything from Animal — its constructor, properties and methods — and adds fly. A Bird can be used anywhere an Animal is expected.

Calling super

If a subclass defines its own constructor, it must call super(...) before touching this:

TS super.ts
class Employee {
  constructor(public name: string, protected salary: number) {}
}

class Manager extends Employee {
  constructor(name: string, salary: number, public reports: string[]) {
    super(name, salary);
  }

  bonus() {
    return this.salary * 0.1 * this.reports.length;
  }
}

const m = new Manager("Zoe", 90000, ["Ali", "Bo"]);
console.log(m.bonus());
Output
18000

salary is protected: the subclass Manager can read it, but code outside the class hierarchy can't. That's the difference from private, which even subclasses can't access.

Overriding methods

A subclass can replace an inherited method. Marking it with override tells TypeScript you mean to replace something that exists:

TS override.ts
class Notifier {
  send(msg: string) {
    return `generic: ${msg}`;
  }
}

class EmailNotifier extends Notifier {
  override send(msg: string) {
    return `email: ${msg} (` + super.send(msg) + ")";
  }
}

console.log(new EmailNotifier().send("hi"));
Output
email: hi (generic: hi)

super.send(...) calls the parent's version. With the compiler option noImplicitOverride turned on, override becomes required, and a misspelt or removed parent method is caught:

TS override-typo.ts
class Notifier {
  send(msg: string) {}
}

class SmsNotifier extends Notifier {
  override sned(msg: string) {}
}
tsc output
error TS4113: This member cannot have an 'override' modifier because it is not declared in the base class 'Notifier'.

An override must also stay compatible with the parent's signature — you can't, for example, change a parameter from string to number.

Abstract classes

An abstract class is a partial class: it can contain real code, but also abstract members that subclasses must provide. You can't create an instance of it directly:

TS abstract.ts
abstract class Report {
  abstract title: string;
  abstract rows(): string[];

  print() {
    console.log(`== ${this.title} ==`);
    this.rows().forEach(r => console.log(r));
  }
}

const r = new Report();
tsc output
error TS2511: Cannot create an instance of an abstract class.

Concrete subclasses fill in the blanks:

TS abstract-impl.ts
abstract class Report {
  abstract title: string;
  abstract rows(): string[];

  print() {
    console.log(`== ${this.title} ==`);
    this.rows().forEach(r => console.log(r));
  }
}

class SalesReport extends Report {
  title = "Sales";
  rows() {
    return ["Mon: 12", "Tue: 18"];
  }
}

new SalesReport().print();
Output
== Sales ==
Mon: 12
Tue: 18

If SalesReport forgot to implement rows, it would be a compile error. This "template method" pattern — shared logic in the base, specific steps in subclasses — is the main reason to use an abstract class.

Abstract class or interface?

  • An interface is only a type. It has no code and disappears at compile time. A class can implement many.
  • An abstract class exists at runtime, can hold shared method implementations and constructor logic, and a class can extend only one.

Use an interface to describe a capability, and an abstract class when subclasses genuinely share code.

Favour composition when inheritance gets deep

Inheritance chains longer than two or three levels tend to become hard to follow, because behaviour is spread across many files. Often it's simpler to give a class the objects it needs (for example, passing a Logger into the constructor) rather than inheriting from a LoggingBase class.

Try it yourself

Try it yourself
Console output