DEV Community

Cover image for Liskov Substitution Principle In Typescript: Explained
Hasan Zohdy
Hasan Zohdy

Posted on

Liskov Substitution Principle In Typescript: Explained

Introduction

Liskov Substitution Principle is the third principle in S.O.L.I.D principles. It was introduced by Barbara Liskov in 1987. It is a principle in object-oriented programming that states that if a program is using a base class, then the reference to the base class can be replaced with a derived class without affecting the functionality of the program.

Problem

Let's say we have a Rectangle class that has a width and height properties and a getArea method that returns the area of the rectangle.

class Rectangle {
  constructor(public width: number, public height: number) {}

  getArea(): number {
    return this.width * this.height;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we want to create a Square class that extends the Rectangle class. The Square class has a size property and overrides the getArea method to return the area of the square.

class Square extends Rectangle {
  constructor(public size: number) {
    super(size, size);
  }

  getArea(): number {
    return this.size * this.size;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's create a function that takes a Rectangle object and prints its area.

function printArea(rectangle: Rectangle) {
  console.log(rectangle.getArea());
}
Enter fullscreen mode Exit fullscreen mode

Now let's create a Rectangle object and pass it to the printArea function.

const rectangle = new Rectangle(2, 3);

printArea(rectangle); // 6
Enter fullscreen mode Exit fullscreen mode

Now let's create a Square object and pass it to the printArea function.

const square = new Square(2);

printArea(square); // 4
Enter fullscreen mode Exit fullscreen mode

As you can see, the printArea function works fine with both Rectangle and Square objects. But what if we want to create a Square object and pass it to the printArea function as a Rectangle object?

Although this works fine, but we had to override the getArea method in the Square class to make it work. This is a violation of the Liskov Substitution Principle. The Square class is not a proper substitute for the Rectangle class.

Solution

To make it properly addressed, we can do it using base class.

Using A Shape as Base Class

Let's make a base class Shape this is basically an abstract class with abstract getArea method.

abstract class Shape {
  abstract getArea(): number;
}
Enter fullscreen mode Exit fullscreen mode

Now let's make the Rectangle and Square classes extend the Shape class.

class Rectangle extends Shape {
  constructor(public width: number, public height: number) {
    super();
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(public size: number) {
    super();
  }

  getArea(): number {
    return this.size * this.size;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's create a function that takes a Shape object and prints its area.

function printArea(shape: Shape) {
  console.log(shape.getArea());
}
Enter fullscreen mode Exit fullscreen mode

Another way to solve this problem

We can also solve this problem by using an interface.

interface Shape {
  getArea(): number;
}
Enter fullscreen mode Exit fullscreen mode

Now let's make the Rectangle and Square classes implement the Shape interface.

class Rectangle implements Shape {
  constructor(public width: number, public height: number) {}

  getArea(): number {
    return this.width * this.height;
  }
}

class Square implements Shape {
  constructor(public size: number) {}

  getArea(): number {
    return this.size * this.size;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's create a function that takes a Shape object and prints its area.

function printArea(shape: Shape) {
  console.log(shape.getArea());
}
Enter fullscreen mode Exit fullscreen mode

More Example

Imagine that we have a Vehicle class that has a fuelUp and maxFuelCapacity methods

class Vehicle {
    public fuelUp(): string {
        return 'Fueled';
    }

    public maxFuelCapacity(): number {
        return 100;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now Let's create Car class which inherits the Vehicle class, pretty much the same methods in our case.

class Car extends Vehicle {
    // nothing to do
}
Enter fullscreen mode Exit fullscreen mode

Let's use it with a function fuelUpVehicle that takes a Vehicle object and calls the fuelUp method.

function fuelUpVehicle(vehicle: Vehicle): void {
    vehicle.fuelUp();
}
Enter fullscreen mode Exit fullscreen mode

Now let's create a Car object and pass it to the fuelUpVehicle function.

const car = new Car();

fuelUpVehicle(car);
Enter fullscreen mode Exit fullscreen mode

So far so good, now let's create another vehicle, a ElectricCar this time, which also inherits the Vehicle class, but it doesn't need the fuelUp method, because it doesn't use fuel.

class ElectricCar extends Vehicle {
    public fuelUp(): string {
        throw new Error('Electric car does not need fuel');
    }

    public maxFuelCapacity(): number {
        return 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

If we tried to use the same function fuelUpVehicle with the ElectricCar object, it will throw an error.

const electricCar = new ElectricCar();

fuelUpVehicle(electricCar); // Error: Electric car does not need fuel
Enter fullscreen mode Exit fullscreen mode

This violates the Liskov Substitution Principle because ElectricCar is not a proper Substitution for the Vehicle car.

Let's fix this behavior by changing the ElectricCar class

class ElectricCar extends Vehicle {
    public fuelUp(): string {
        return 'Electric car is charged';
    }

    public maxFuelCapacity(): number {
        return 100; // this is the battery capacity in percentage not in liters
    }
}
Enter fullscreen mode Exit fullscreen mode

Now if we tried to use the same function fuelUpVehicle with the ElectricCar object, it will work fine.

const electricCar = new ElectricCar();

fuelUpVehicle(electricCar); // Electric car is charged
Enter fullscreen mode Exit fullscreen mode

Conclusion

To address Liskov Substitution Principle (LSP) we can say that the base class should be able to be substituted with any of its derived classes without affecting the functionality of the program.

In other words, Subtypes must be substitutable for their base types. In simpler terms, if a piece of code works with objects of the base class, it should work equally well (without surprises) with objects of any derived class.

Following the list

You can see the updated list of design principles from the following link
https://mentoor.io/en/posts/634524154/open-closed-principle-in-typescript

Join us in our Discord Community
https://discord.gg/XDZcTuU8c8

Top comments (0)