Inheritance and interfaces
In this chapter, you learn about class inheritance and interfaces in TypeScript.
What you learn in this chapter
- How to use inheritance (extends)
- How to define an abstract class (abstract)
- The basics of interfaces (interface)
- Implementing an interface with the implements keyword
You can try the code examples in this chapter on the TypeScript Playground.
Inheritance (extends)
Inheritance is a mechanism for creating a new class by taking over the features of an existing class. It improves code reusability and makes the relationships between classes clear.
Basic inheritance
// Parent class (base class, superclass)
class Animal {
constructor(
public name: string,
protected age: number
) {}
makeSound(): void {
console.log('Some generic sound');
}
sleep(): void {
console.log(`${this.name} is sleeping`);
}
}
// Child class (derived class, subclass)
class Dog extends Animal {
constructor(name: string, age: number, public breed: string) {
// Call the parent class's constructor (required)
super(name, age);
}
// Method override
makeSound(): void {
console.log('Woof! Woof!');
}
// A method unique to the child class
fetch(): void {
console.log(`${this.name} is fetching the ball`);
}
}
class Cat extends Animal {
constructor(name: string, age: number) {
super(name, age);
}
makeSound(): void {
console.log('Meow! Meow!');
}
scratch(): void {
console.log(`${this.name} is scratching`);
}
}
// Usage
const dog = new Dog('Buddy', 3, 'Golden Retriever');
dog.makeSound(); // 'Woof! Woof!'
dog.sleep(); // 'Buddy is sleeping' (a method of the parent class)
dog.fetch(); // 'Buddy is fetching the ball'
const cat = new Cat('Whiskers', 2);
cat.makeSound(); // 'Meow! Meow!'
cat.sleep(); // 'Whiskers is sleeping'
cat.scratch(); // 'Whiskers is scratching'
Code walkthrough:
- The
extendskeyword declares inheritance super(...)calls the parent class's constructor (required first in the child class's constructor)- You can override (replace) the parent class's methods
- The parent class's
publicandprotectedmembers are accessible privatemembers are not accessible even from the child class
Calling a parent class's method with super
Inside an overridden method, you can also call the parent class's implementation.
class Vehicle {
constructor(
public brand: string,
protected speed: number = 0
) {}
accelerate(amount: number): void {
this.speed += amount;
console.log(`Speed increased to ${this.speed} km/h`);
}
brake(amount: number): void {
this.speed = Math.max(0, this.speed - amount);
console.log(`Speed decreased to ${this.speed} km/h`);
}
}
class Car extends Vehicle {
constructor(brand: string, private model: string) {
super(brand);
}
// Override the parent class's method while also running the parent's logic
accelerate(amount: number): void {
console.log(`${this.brand} ${this.model} is accelerating...`);
// Call the parent class's accelerate method
super.accelerate(amount);
}
showInfo(): void {
console.log(`Car: ${this.brand} ${this.model}, Speed: ${this.speed} km/h`);
}
}
const car = new Car('Toyota', 'Camry');
car.accelerate(50);
// 'Toyota Camry is accelerating...'
// 'Speed increased to 50 km/h'
car.accelerate(30);
// 'Toyota Camry is accelerating...'
// 'Speed increased to 80 km/h'
car.showInfo(); // 'Car: Toyota Camry, Speed: 80 km/h'
Abstract classes (abstract)
An abstract class is a class that cannot be instantiated, and it defines methods that derived classes must implement. It works as a "design framework."
Abstract class basics
// Abstract class: add the abstract keyword
abstract class Shape {
constructor(public name: string) {}
// Abstract method: has no implementation; derived classes must implement it
abstract calculateArea(): number;
abstract calculatePerimeter(): number;
// A normal method: has an implementation
describe(): void {
console.log(`This is a ${this.name}`);
console.log(`Area: ${this.calculateArea()}`);
console.log(`Perimeter: ${this.calculatePerimeter()}`);
}
}
// An abstract class cannot be instantiated
// const shape = new Shape('shape'); // Error: Cannot create an instance of an abstract class
// A class that inherits from the abstract class
class Circle extends Shape {
constructor(private radius: number) {
super('Circle');
}
// Implement the abstract methods (required)
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
calculatePerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(
private width: number,
private height: number
) {
super('Rectangle');
}
calculateArea(): number {
return this.width * this.height;
}
calculatePerimeter(): number {
return 2 * (this.width + this.height);
}
}
// Usage
const circle = new Circle(10);
circle.describe();
// 'This is a Circle'
// 'Area: 314.159...'
// 'Perimeter: 62.831...'
const rectangle = new Rectangle(5, 10);
rectangle.describe();
// 'This is a Rectangle'
// 'Area: 50'
// 'Perimeter: 30'
Characteristics of abstract classes:
- Defined with the
abstractkeyword - Cannot be instantiated (you inherit from them to use them)
- Abstract methods have no implementation (just a declaration)
- Derived classes must implement the abstract methods
- You can also define normal methods (with implementations)
A practical example: payment processing
// An abstract Payment class
abstract class Payment {
constructor(
protected amount: number,
protected currency: string = 'JPY'
) {}
// Abstract methods: the implementation differs per payment method
abstract process(): Promise<boolean>;
abstract getReceipt(): string;
// A shared method
getAmount(): string {
return `${this.amount} ${this.currency}`;
}
protected logTransaction(success: boolean): void {
const status = success ? 'SUCCESS' : 'FAILED';
console.log(`[${new Date().toISOString()}] Payment ${status}: ${this.getAmount()}`);
}
}
// Credit card payment
class CreditCardPayment extends Payment {
constructor(
amount: number,
private cardNumber: string,
private expiryDate: string
) {
super(amount);
}
async process(): Promise<boolean> {
// In reality, you would call the card company's API
console.log(`Processing credit card payment: ${this.maskCardNumber()}`);
const success = true; // simulation
this.logTransaction(success);
return success;
}
getReceipt(): string {
return `Credit Card Payment: ${this.getAmount()} (Card: ${this.maskCardNumber()})`;
}
private maskCardNumber(): string {
return '**** **** **** ' + this.cardNumber.slice(-4);
}
}
// Bank transfer
class BankTransferPayment extends Payment {
constructor(
amount: number,
private bankCode: string,
private accountNumber: string
) {
super(amount);
}
async process(): Promise<boolean> {
console.log(`Processing bank transfer to: ${this.bankCode}-${this.accountNumber}`);
const success = true;
this.logTransaction(success);
return success;
}
getReceipt(): string {
return `Bank Transfer: ${this.getAmount()} (Account: ${this.accountNumber})`;
}
}
// Usage
async function processPayments() {
const payments: Payment[] = [
new CreditCardPayment(5000, '1234567890123456', '12/25'),
new BankTransferPayment(10000, '0001', '1234567')
];
for (const payment of payments) {
await payment.process();
console.log(payment.getReceipt());
console.log('---');
}
}
processPayments();
Interfaces (interface)
An interface is a mechanism for defining the "shape" or "contract" of an object. It declares what structure a class or value should have.
Interface basics
// Defining an interface
interface User {
id: number;
name: string;
email: string;
age?: number; // optional property
}
// An object that satisfies the interface
const user1: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
age: 25
};
const user2: User = {
id: 2,
name: 'Bob',
email: 'bob@example.com'
// age can be omitted
};
// It can also be used as a function's parameter type
function printUser(user: User): void {
console.log(`${user.id}: ${user.name} (${user.email})`);
}
printUser(user1); // '1: Alice (alice@example.com)'
printUser(user2); // '2: Bob (bob@example.com)'
Interfaces with methods
interface Printable {
print(): void;
}
interface Saveable {
save(): boolean;
}
// An object that satisfies the interfaces
const document: Printable & Saveable = {
print() {
console.log('Printing document...');
},
save() {
console.log('Saving document...');
return true;
}
};
document.print(); // 'Printing document...'
document.save(); // 'Saving document...'
Interface inheritance
An interface can inherit from other interfaces.
interface Named {
name: string;
}
interface Aged {
age: number;
}
// Inherit from multiple interfaces
interface Person extends Named, Aged {
email: string;
}
const person: Person = {
name: 'Alice',
age: 25,
email: 'alice@example.com'
};
The implements keyword
A class can implement (implements) an interface. This guarantees that the class satisfies the contract defined by the interface.
Basic usage
interface Printable {
print(): void;
}
interface Saveable {
save(): void;
}
// Implement multiple interfaces
class Document implements Printable, Saveable {
constructor(private content: string) {}
// Implement the Printable interface's method
print(): void {
console.log(`Printing: ${this.content}`);
}
// Implement the Saveable interface's method
save(): void {
console.log(`Saving: ${this.content}`);
}
}
const doc = new Document('Hello, World!');
doc.print(); // 'Printing: Hello, World!'
doc.save(); // 'Saving: Hello, World!'
A practical example: the repository pattern
// An interface for data operations
interface Repository<T> {
findAll(): T[];
findById(id: number): T | undefined;
create(item: T): T;
update(id: number, item: Partial<T>): T | undefined;
delete(id: number): boolean;
}
// The user entity
interface User {
id: number;
name: string;
email: string;
}
// A User repository that works in memory
class InMemoryUserRepository implements Repository<User> {
private users: User[] = [];
private nextId: number = 1;
findAll(): User[] {
return [...this.users];
}
findById(id: number): User | undefined {
return this.users.find(user => user.id === id);
}
create(item: Omit<User, 'id'>): User {
const newUser: User = {
id: this.nextId++,
...item
};
this.users.push(newUser);
return newUser;
}
update(id: number, item: Partial<User>): User | undefined {
const index = this.users.findIndex(user => user.id === id);
if (index === -1) return undefined;
this.users[index] = { ...this.users[index], ...item };
return this.users[index];
}
delete(id: number): boolean {
const index = this.users.findIndex(user => user.id === id);
if (index === -1) return false;
this.users.splice(index, 1);
return true;
}
}
// Usage
const userRepo = new InMemoryUserRepository();
const alice = userRepo.create({ name: 'Alice', email: 'alice@example.com' });
console.log(alice); // { id: 1, name: 'Alice', email: 'alice@example.com' }
const bob = userRepo.create({ name: 'Bob', email: 'bob@example.com' });
console.log(userRepo.findAll()); // [alice, bob]
userRepo.update(1, { name: 'Alicia' });
console.log(userRepo.findById(1)); // { id: 1, name: 'Alicia', email: 'alice@example.com' }
userRepo.delete(2);
console.log(userRepo.findAll()); // [alice]
Abstract class vs. interface
| Feature | Abstract class | Interface |
|---|---|---|
| Instantiation | Not possible | Not possible |
| Implementation | Can have | Cannot have (type definition only) |
| Inheritance | Single inheritance only | Can implement multiple |
| Constructor | Can have | Cannot have |
| Access modifiers | Can use | Cannot use |
| Use | Provide a shared implementation | Define a contract / type |
// Abstract class: has a shared implementation
abstract class Animal {
constructor(protected name: string) {}
// Shared implementation
sleep(): void {
console.log(`${this.name} is sleeping`);
}
// Abstract method
abstract makeSound(): void;
}
// Interface: contract only
interface Flyable {
fly(): void;
}
// Use both
class Bird extends Animal implements Flyable {
constructor(name: string) {
super(name);
}
makeSound(): void {
console.log('Tweet!');
}
fly(): void {
console.log(`${this.name} is flying`);
}
}
const bird = new Bird('Sparrow');
bird.sleep(); // 'Sparrow is sleeping' (inherited from the abstract class)
bird.makeSound(); // 'Tweet!' (implements the abstract method)
bird.fly(); // 'Sparrow is flying' (implements the interface)
Try it: design a set of shape classes ★★★
Design a set of shape classes that meet the following requirements.
Requirements:
-
The
Drawableinterface:- a
draw(): voidmethod
- a
-
The abstract class
Shape:- a
name: stringproperty (readonly) - an
abstract calculateArea(): numberabstract method - an
abstract calculatePerimeter(): numberabstract method - a
describe(): voidmethod (display the name, area, and perimeter) - implements the
Drawableinterface
- a
-
The
Circleclass (inherits Shape):- a
radius: numberproperty - area = π × r²
- perimeter = 2 × π × r
- a
-
The
Rectangleclass (inherits Shape):width: number,height: numberproperties- area = width × height
- perimeter = 2 × (width + height)
-
The
Triangleclass (inherits Shape):a: number,b: number,c: number(the lengths of the three sides)- area = use Heron's formula
- perimeter = a + b + c
Hint
- Heron's formula: s = (a + b + c) / 2, area = √(s × (s-a) × (s-b) × (s-c))
- In each shape's
draw()method, it is nice to display the shape as ASCII art - Make use of the shorthand constructor syntax
Answer and explanation
// An interface for drawable objects
interface Drawable {
draw(): void;
}
// The abstract class for shapes
abstract class Shape implements Drawable {
constructor(public readonly name: string) {}
// Abstract methods: must be implemented by derived classes
abstract calculateArea(): number;
abstract calculatePerimeter(): number;
// Implement the Drawable interface (it can also be delegated to derived classes as an abstract method)
abstract draw(): void;
// A shared method
describe(): void {
console.log(`=== ${this.name} ===`);
console.log(`Area: ${this.calculateArea().toFixed(2)}`);
console.log(`Perimeter: ${this.calculatePerimeter().toFixed(2)}`);
}
}
// The Circle class
class Circle extends Shape {
constructor(private radius: number) {
super('Circle');
}
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
calculatePerimeter(): number {
return 2 * Math.PI * this.radius;
}
draw(): void {
// Simple ASCII art
console.log(' *** ');
console.log(' * * ');
console.log(' * * ');
console.log(' * * ');
console.log(' * * ');
console.log(' *** ');
}
}
// The Rectangle class
class Rectangle extends Shape {
constructor(
private width: number,
private height: number
) {
super('Rectangle');
}
calculateArea(): number {
return this.width * this.height;
}
calculatePerimeter(): number {
return 2 * (this.width + this.height);
}
draw(): void {
const horizontalLine = '+' + '-'.repeat(this.width) + '+';
console.log(horizontalLine);
for (let i = 0; i < Math.min(this.height, 5); i++) {
console.log('|' + ' '.repeat(this.width) + '|');
}
console.log(horizontalLine);
}
}
// The Triangle class
class Triangle extends Shape {
constructor(
private a: number,
private b: number,
private c: number
) {
super('Triangle');
// Check the triangle inequality
if (!this.isValidTriangle()) {
throw new Error('Invalid triangle: sides do not form a valid triangle');
}
}
private isValidTriangle(): boolean {
return (
this.a + this.b > this.c &&
this.b + this.c > this.a &&
this.c + this.a > this.b
);
}
calculateArea(): number {
// Heron's formula
const s = (this.a + this.b + this.c) / 2;
return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
}
calculatePerimeter(): number {
return this.a + this.b + this.c;
}
draw(): void {
console.log(' * ');
console.log(' * * ');
console.log(' * * ');
console.log(' * * ');
console.log('*********');
}
}
// A function that makes use of polymorphism
function displayShapes(shapes: Shape[]): void {
for (const shape of shapes) {
shape.draw();
shape.describe();
console.log('');
}
}
// Test
const shapes: Shape[] = [
new Circle(5),
new Rectangle(8, 4),
new Triangle(3, 4, 5) // a right triangle
];
displayShapes(shapes);
// Individual tests
console.log('=== Individual Tests ===');
const circle = new Circle(10);
console.log(`Circle area: ${circle.calculateArea().toFixed(2)}`); // 314.16
console.log(`Circle perimeter: ${circle.calculatePerimeter().toFixed(2)}`); // 62.83
const rectangle = new Rectangle(5, 10);
console.log(`Rectangle area: ${rectangle.calculateArea()}`); // 50
console.log(`Rectangle perimeter: ${rectangle.calculatePerimeter()}`); // 30
const triangle = new Triangle(3, 4, 5);
console.log(`Triangle area: ${triangle.calculateArea()}`); // 6
console.log(`Triangle perimeter: ${triangle.calculatePerimeter()}`); // 12
// Error case: an invalid triangle
try {
const invalidTriangle = new Triangle(1, 2, 10); // does not form a triangle
} catch (error) {
console.log((error as Error).message);
// 'Invalid triangle: sides do not form a valid triangle'
}
Explanation:
- The
Drawableinterface: defines the contract for thedraw()method - The
Shapeabstract class: defines the shareddescribe()method and the abstract methods - Each shape class: implements the abstract methods concretely
- The
Triangleclass: validates the triangle inequality in its constructor - The
displayShapesfunction: through polymorphism, processes different shapes in the same way
Benefits of polymorphism:
- A
Shape[]array can handle different shapes uniformly - Adding a new shape class does not require changing existing code
- Depending on the interface lets you hide implementation details
Summary
What you learned in this chapter:
- Inheritance (extends): create a new class by taking over the features of an existing class
- super: call the parent class's constructor or methods
- Abstract classes (abstract): a design framework that cannot be instantiated; abstract methods must be implemented by derived classes
- Interfaces (interface): define the shape or contract of an object
- implements: declare that a class implements an interface
By combining these concepts, you can write flexible and extensible code. In the next chapter, you will learn about type narrowing.
Common pitfalls for beginners
Common mistakes
❌ Forgetting to call super in the child class
class Animal {
constructor(public name: string) {}
}
// ❌ Wrong: super is not called
class Dog extends Animal {
constructor(name: string, public breed: string) {
// Error: Constructors for derived classes must contain a 'super' call
this.breed = breed;
}
}
// ✅ Correct: call super first
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // call the parent class's constructor first
}
}
Cause: In a child class's constructor, you must call super() before using this.
Solution: Call super() on the first line of the child class's constructor.
❌ Accessing a private property from a child class
class Parent {
private secret: string = 'hidden';
protected info: string = 'visible to children';
}
class Child extends Parent {
showInfo() {
// console.log(this.secret); // Error: private is not accessible from the child class
console.log(this.info); // OK: protected is accessible from the child class
}
}
Cause: private is accessible only inside the class where it is defined, not even from a child class.
Solution: Use protected when you want to give access from a child class.
❌ Trying to instantiate an abstract class
abstract class Shape {
abstract calculateArea(): number;
}
// ❌ Wrong: an abstract class cannot be instantiated
const shape = new Shape();
// Error: Cannot create an instance of an abstract class
// ✅ Correct: inherit from it with a concrete class and implement it
class Circle extends Shape {
constructor(private radius: number) {
super();
}
calculateArea(): number {
return Math.PI * this.radius ** 2;
}
}
const circle = new Circle(5); // OK
Cause: An abstract class is a "blueprint for a blueprint" and is not meant to be instantiated directly.
Solution: Create a concrete class that inherits from the abstract class, and instantiate that class.
❌ Not understanding the difference between interface and type
// interface: extensible (same-name declarations are merged)
interface User {
name: string;
}
interface User {
age: number; // same-name interfaces are merged automatically
}
const user: User = { name: 'Alice', age: 25 }; // OK
// type: not extensible (redefining the same name is an error)
type Product = {
name: string;
};
// type Product = { // Error: duplicate identifier
// price: number;
// };
// Extend a type with an intersection
type ExtendedProduct = Product & { price: number };
Cause: interface merges same-name declarations, but type cannot be redefined.
Solution: Prefer interface for defining object shapes, and use type for union types and utility types.
❌ Confusing implements with extends
interface Printable {
print(): void;
}
class Document {
content: string = '';
}
// ❌ Wrong: trying to extends an interface (cannot be used for a class)
// class Report extends Printable {} // Error
// ✅ Correct: implement an interface with implements
class Report implements Printable {
print(): void {
console.log('Printing report...');
}
}
// ✅ Correct: inherit a class with extends
class PDFDocument extends Document implements Printable {
print(): void {
console.log('Printing PDF...');
}
}
Cause: extends is for class inheritance, while implements is for implementing an interface.
Solution: Use extends for class inheritance and implements for implementing an interface.
Decorators (TC39 standard spec / TS 5.0+)
Decorators are a syntax for declaratively attaching metadata or behavior to classes, methods, and properties. In TypeScript 5.0 and later, the TC39 Stage 3 standard spec is available as stable.
// An example decorator that logs a method call
function log<T extends (this: unknown, ...args: unknown[]) => unknown>(
target: T,
context: ClassMethodDecoratorContext,
): T {
return function (this: unknown, ...args: unknown[]) {
console.log(`Calling ${String(context.name)} with`, args)
const result = target.apply(this, args)
console.log(`→ result:`, result)
return result
} as T
}
class Calculator {
@log
add(a: number, b: number) {
return a + b
}
}
new Calculator().add(2, 3)
// Calling add with [ 2, 3 ]
// → result: 5
The difference from the old decorators
TypeScript has long had an older implementation called experimental decorators (--experimentalDecorators), but it is not compatible with the TC39 standard.
| Item | Standard decorators (TS 5.0+) | experimental decorators (legacy) |
|---|---|---|
| Spec | TC39 Stage 3 | An old proposal from around 2015 |
| Enabling | Available by default | Requires --experimentalDecorators |
| Type arguments | Standard types like ClassMethodDecoratorContext | Its own (target, key, descriptor) |
| Metadata | Symbol.metadata is available (TS 5.2+; a polyfill is needed on unsupported runtimes) | Depends on the reflect-metadata library |
| Future | Planned to be adopted into the JavaScript standard | Being deprecated |
If you are using --experimentalDecorators in existing code
Some frameworks like Angular, NestJS, and TypeORM still require legacy decorators. Following your framework's migration plan, it is fine to keep --experimentalDecorators enabled until a supported version is released. For new projects, though, choose the TC39 standard decorators.
What to read next
Move on to type narrowing. You will learn how to safely narrow types with union types, type guards (typeof / in / instanceof), and tagged unions.
- Type narrowing — handle union types safely with type guards