Inheritance and Class Hierarchies in TypeScript

Table of Contents

  • Introduction
  • What is Inheritance in TypeScript?
  • Single Inheritance in TypeScript
    • Extending Classes
    • Overriding Methods
  • The super Keyword in Inheritance
  • Multiple Inheritance (Simulated through Interfaces)
  • Class Hierarchies: Building Relationships Between Classes
  • Abstract Classes and Their Role in Inheritance
  • Constructor Inheritance and Initialization
  • Conclusion

Introduction

Inheritance is one of the core concepts of object-oriented programming (OOP) that allows a class to inherit properties and methods from another class. In TypeScript, inheritance helps create hierarchical relationships between classes, making it possible to reuse and extend existing code. TypeScript builds upon JavaScript’s prototypal inheritance by introducing features such as access modifiers, abstract classes, and interfaces to provide a more structured approach to inheritance.

In this article, we will dive deep into inheritance and class hierarchies in TypeScript, explaining how inheritance works, how to extend classes, override methods, and simulate multiple inheritance. We will also discuss the role of abstract classes and how to implement constructor inheritance.


What is Inheritance in TypeScript?

Inheritance is a mechanism where a new class (called a subclass or child class) derives properties and methods from an existing class (called a superclass or parent class). The subclass can inherit the behavior of the parent class and also define additional properties or override the inherited methods.

In TypeScript, inheritance is achieved using the extends keyword. A child class can extend a parent class, gaining access to its public and protected members.

Example of Inheritance

class Animal {
name: string;

constructor(name: string) {
this.name = name;
}

speak(): void {
console.log(`${this.name} makes a sound.`);
}
}

class Dog extends Animal {
constructor(name: string) {
super(name); // Calls the constructor of the parent class
}

speak(): void {
console.log(`${this.name} barks.`);
}
}

const dog = new Dog("Buddy");
dog.speak(); // Output: Buddy barks.

In this example:

  • The Dog class extends the Animal class.
  • The Dog class inherits the name property and speak() method from Animal.
  • The Dog class overrides the speak() method to provide a custom implementation for dogs.

Single Inheritance in TypeScript

TypeScript supports single inheritance, where a class can only inherit from one parent class. However, TypeScript allows you to simulate multiple inheritance through interfaces, which we’ll cover later.

Extending Classes

When a class extends another, it inherits the properties and methods of the parent class. However, it cannot inherit private members of the parent class. Only public and protected members are accessible in the child class.

Example of Extending a Class

class Person {
protected name: string;

constructor(name: string) {
this.name = name;
}

greet(): void {
console.log(`Hello, ${this.name}!`);
}
}

class Employee extends Person {
private jobTitle: string;

constructor(name: string, jobTitle: string) {
super(name); // Calls the constructor of Person
this.jobTitle = jobTitle;
}

displayJobTitle(): void {
console.log(`${this.name} is a(n) ${this.jobTitle}.`);
}
}

const employee = new Employee("John", "Software Engineer");
employee.greet(); // Output: Hello, John!
employee.displayJobTitle(); // Output: John is a(n) Software Engineer.

Here:

  • The Employee class extends Person and inherits the name property and greet() method.
  • The Employee class introduces a new method, displayJobTitle(), and a private jobTitle property.
  • The super() function is used in the Employee constructor to call the parent class’s constructor.

The super Keyword in Inheritance

The super keyword plays a critical role in class inheritance. It allows you to access the parent class’s methods and properties, as well as call its constructor.

Using super in Constructor

In the example above, the super(name) call inside the Employee class constructor is used to invoke the Person class’s constructor, ensuring the name property is correctly initialized.

Using super to Call Methods

You can also use super to call methods defined in the parent class. This is useful when you want to override a method but still keep the behavior from the parent class.

Example of Using super to Call Methods

class Animal {
sound(): void {
console.log("Animal makes a sound");
}
}

class Dog extends Animal {
sound(): void {
super.sound(); // Call the parent class's method
console.log("Dog barks");
}
}

const dog = new Dog();
dog.sound(); // Output: Animal makes a sound \n Dog barks

In this example:

  • The Dog class overrides the sound() method but calls the parent class’s sound() method using super.

Multiple Inheritance (Simulated through Interfaces)

TypeScript does not support multiple inheritance of classes. A class can only extend one parent class. However, TypeScript allows you to achieve multiple inheritance through interfaces.

A class can implement multiple interfaces, inheriting the contract of each interface. This is how TypeScript simulates multiple inheritance.

Example of Multiple Inheritance Using Interfaces

interface Flyable {
fly(): void;
}

interface Swimmable {
swim(): void;
}

class Duck implements Flyable, Swimmable {
fly(): void {
console.log("Duck is flying");
}

swim(): void {
console.log("Duck is swimming");
}
}

const duck = new Duck();
duck.fly(); // Output: Duck is flying
duck.swim(); // Output: Duck is swimming

In this example:

  • The Duck class implements two interfaces: Flyable and Swimmable.
  • This allows the Duck class to have both fly() and swim() methods.

Class Hierarchies: Building Relationships Between Classes

Class hierarchies allow you to establish relationships between classes. This is useful when you have a base class that contains common functionality, and derived classes that add or modify specific behavior.

Example of Class Hierarchy

class Vehicle {
move(): void {
console.log("Vehicle is moving");
}
}

class Car extends Vehicle {
move(): void {
console.log("Car is driving");
}
}

class Bike extends Vehicle {
move(): void {
console.log("Bike is cycling");
}
}

const car = new Car();
const bike = new Bike();

car.move(); // Output: Car is driving
bike.move(); // Output: Bike is cycling

In this example:

  • Vehicle is the base class, and Car and Bike are subclasses that extend Vehicle.
  • Each subclass provides its own implementation of the move() method, creating a polymorphic behavior.

Abstract Classes and Their Role in Inheritance

An abstract class is a class that cannot be instantiated directly. It can be extended by other classes, which must implement its abstract methods. Abstract classes allow you to define methods that must be implemented by subclasses, ensuring that certain functionality is always provided.

Example of Abstract Class

abstract class Shape {
abstract area(): number;
}

class Circle extends Shape {
radius: number;

constructor(radius: number) {
super();
this.radius = radius;
}

area(): number {
return Math.PI * this.radius ** 2;
}
}

const circle = new Circle(5);
console.log(circle.area()); // Output: 78.53981633974483

In this example:

  • Shape is an abstract class with an abstract area() method.
  • Circle extends Shape and implements the area() method.

You cannot create an instance of the Shape class directly; it must be subclassed and the abstract methods implemented.


Constructor Inheritance and Initialization

When a subclass extends a parent class, the subclass inherits the constructor of the parent class. However, if the parent class’s constructor requires arguments, the subclass must call the parent constructor using the super() keyword.

Example of Constructor Inheritance

class Animal {
constructor(public name: string) {}

speak(): void {
console.log(`${this.name} speaks.`);
}
}

class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // Calls the parent class's constructor
}

speak(): void {
console.log(`${this.name} the ${this.breed} barks.`);
}
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // Output: Buddy the Golden Retriever barks.

In this example:

  • The Dog class extends Animal and calls the Animal constructor using super(name).
  • The Dog class adds an additional property breed and overrides the speak() method.

Conclusion

Inheritance is a cornerstone of object-oriented programming, and TypeScript’s class system provides a powerful way to implement it. By using the extends keyword, TypeScript allows you to create hierarchies where child classes can inherit functionality from parent classes. You can also use abstract classes and interfaces to define reusable structures and enforce implementation contracts across your codebase.

With the ability to simulate multiple inheritance via interfaces and leveraging the super keyword for calling parent methods and constructors, TypeScript provides a robust model for building complex class hierarchies.