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.
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:
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"));
}Email to Asha: Your order has shipped today
SMS to Asha: Your order has shippCode 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 {}. implementsonly 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:
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()));Circle with area 3.14
Square with area 9.00The compiler enforces both rules:
abstract class Shape {
abstract area(): number;
}
const s = new Shape();error TS2511: Cannot create an instance of an abstract class.abstract class Shape {
abstract area(): number;
}
class Triangle extends Shape {}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):
class Base {
greet(): string {
return "hello";
}
}
class Child extends Base {
override greeting(): string {
return "hi";
}
}error TS4113: This member cannot have an 'override' modifier because it is not declared in the base class 'Base'.Interface or abstract class?#
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
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());SALES REPORT
- North: ₹120000
- South: ₹98000Key takeaway#
Finished this lesson?