Skip to content
Laviyx Learn

Try

TypeScript course menu

TypeScript Abstract Classes and implements: Designing Class Contracts

Learn abstract classes and the implements keyword in TypeScript: abstract methods and properties, why abstract classes cannot be instantiated, implementing one or more interfaces, override, and when to choose an interface versus an abstract class.

Intermediate8 min readUpdated
On this page

When several classes should share a structure, TypeScript offers two tools: interfaces that classes promise to implement, and abstract classes that provide shared code plus methods that subclasses must fill in.

implements#

A class can declare that it satisfies an interface. The compiler then checks that every member is present with a compatible type:

TypeScript
interface Notifier {
  channel: string;
  send(to: string, message: string): string;
}

class EmailNotifier implements Notifier {
  channel = "email";
  send(to: string, message: string): string {
    return `Email to ${to}: ${message}`;
  }
}

class SmsNotifier implements Notifier {
  channel = "sms";
  send(to: string, message: string): string {
    return `SMS to ${to}: ${message.slice(0, 20)}`;
  }
}

const notifiers: Notifier[] = [new EmailNotifier(), new SmsNotifier()];
for (const n of notifiers) {
  console.log(n.send("Asha", "Your order has shipped today"));
}
Output
Email to Asha: Your order has shipped today
SMS to Asha: Your order has shipp

Code that uses the interface (Notifier[]) works with any implementation, so new channels can be added without changing it.

  • A class can implement several interfaces: class X implements A, B {}.
  • implements only checks the class; it does not add any code or change types of members. Method parameters still need their own annotations.

Abstract classes#

An abstract class cannot be created directly with new. It provides shared code, and declares abstract members that subclasses must implement:

TypeScript
abstract class Shape {
  constructor(public readonly name: string) {}

  abstract area(): number;          // no body: subclasses must provide it

  describe(): string {              // shared code that uses the abstract method
    return `${this.name} with area ${this.area().toFixed(2)}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super("Circle");
  }
  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Square extends Shape {
  constructor(private side: number) {
    super("Square");
  }
  area(): number {
    return this.side ** 2;
  }
}

const shapes: Shape[] = [new Circle(1), new Square(3)];
shapes.forEach((s) => console.log(s.describe()));
Output
Circle with area 3.14
Square with area 9.00

The compiler enforces both rules:

TypeScript
abstract class Shape {
  abstract area(): number;
}
const s = new Shape();
Compiler error
error TS2511: Cannot create an instance of an abstract class.
TypeScript
abstract class Shape {
  abstract area(): number;
}
class Triangle extends Shape {}
Compiler error
error TS2515: Non-abstract class 'Triangle' does not implement inherited abstract member area from class 'Shape'.

override#

The override keyword marks a method that replaces one from the parent class. With the noImplicitOverride compiler option, TypeScript requires it, and reports an error if the parent has no such method (for example after a rename):

TypeScript
class Base {
  greet(): string {
    return "hello";
  }
}
class Child extends Base {
  override greeting(): string {
    return "hi";
  }
}
Compiler error
error TS4113: This member cannot have an 'override' modifier because it is not declared in the base class 'Base'.

Interface or abstract class?#

InterfaceAbstract class
Contains codeNo, only typesYes, shared methods and properties
Exists at runtimeNoYes (a real class)
A class can use severalYes (implements A, B)No, one parent only
ConstructorNoYes
Good forContracts between parts of a systemFamilies of classes that share behaviour

A practical approach: define the contract as an interface (so tests and other implementations can use it easily), and add an abstract base class only when implementations genuinely share code.

Practice#

Practice exercise

Create an abstract class Report with a title, an abstract method rows(): string[], and a method render(): string that returns the title in upper case followed by each row on its own line prefixed with "- ". Implement SalesReport with fixed rows and print its rendering.

Show solution
TypeScript
abstract class Report {
  constructor(protected title: string) {}

  abstract rows(): string[];

  render(): string {
    return [this.title.toUpperCase(), ...this.rows().map((r) => `- ${r}`)].join("\n");
  }
}

class SalesReport extends Report {
  constructor(private sales: Record<string, number>) {
    super("Sales report");
  }

  rows(): string[] {
    return Object.entries(this.sales).map(([region, amount]) => `${region}: ₹${amount}`);
  }
}

console.log(new SalesReport({ North: 120000, South: 98000 }).render());
Output
SALES REPORT
- North: ₹120000
- South: ₹98000

Key takeaway#

Finished this lesson?