DEV Community

Cover image for [Part 1] A Brief Introduction to SOLID Typescript
Taki089.Dang
Taki089.Dang

Posted on

[Part 1] A Brief Introduction to SOLID Typescript

The SOLID principles are a set of design principles in OOP that help create robust, maintainable, and scalable code. Implementing them in Typescript is straightforward because of its support for classes, interfaces, and strong typing.

_In this post, I provide a brief overview of SOLID to help you understand it more easily, and I will dive into each part as soon as I have time _ =))).

Here's a detailed explanation of the SOLID principles with Typescript examples:


S - Single Responsibility Principle (SRP)

A class should have only one reason to change

This means every class should only do one thing. If it need to do multiple things, it should delegate some responsibilities to other class.

For Example:

❌Bad

class UserService {
    createUser(user: string){
        console.log(`User ${user} created`);
    } // UserService

    sendWelcomeEmail(user: string){
        console.log(`Welcome email sent to ${user}`)
    } //EmailService
}
Enter fullscreen mode Exit fullscreen mode

✅Good

class UserService{
    createUser(user: string): string
    {
        console.log(`User ${user} created`);
        return user;
    }
}

class EmailService{
    sendMail(user: string){
        console.log(`Send mail to ${user}`);
    }
}

Enter fullscreen mode Exit fullscreen mode

So what should I do apply SRP in my code correctly and efficially?
=> Group responsibilities based on the business logic.

  • SendMail involves email and communication -> belongs in EmailService.
  • CreateUser involves user creation (or CRUD user) -> belongs in UserServices.

O - Open/Closed Principle (OCP)

Software entities should open for extension, but closed for modification.

This mean you be able to add new functionality without changing existing code.

❌Bad

class DiscountService{
    calculateDiscount(typeOfDiscount:string, amount: number) : number {
        if (typeOfDiscount == 'standard') {
            return amount * 0.1;
        }
        else if (typeOfDiscount == 'premium') {
            return amount * 0.5;
        }
        return 0;
    }
}
let standard = new DiscountService();
standard.calculateDiscount('standard', 10);
Enter fullscreen mode Exit fullscreen mode

Why is it bad?
If you receive a request to add DiamondDiscount with a 100% discount and additional parameters like isMemberShip, you would need to modify the calculateDiscount() method like:

class DiscountService{
    calculateDiscount(typeOfDiscount:string, amount: number, isMemberShip: boolean) : number {
        if (typeOfDiscount == 'standard') {
            return amount * 0.1;
        }
        else if (typeOfDiscount == 'premium') {
            return amount * 0.5;
        }
        else if (typeOfDiscount == 'diamond') {
            if (isMemberShip){
                return amount;
            }
        }
        return 0;
    }
}

let standard = new DiscountService();
standard.calculateDiscount('standard', 10); // => You must change this one if update calculateDiscount
Enter fullscreen mode Exit fullscreen mode

This could potentially affect other methods that are already using calculateDiscount() (you would need to search for any code that uses calculateDiscount and update it to include the isMemberShip.

✅Good

interface Discount {
    calculateDiscount(amount: number) : number;
}

class StandardDiscount implements Discount {
    calculateDiscount(amount: number): number {
        return amount * 0.1;
    }
}

class DiamondDiscount implements Discount {
    private isMemberShip: boolean = false;
    constructor(isMemberShip: boolean) {
    }
    calculateDiscount(amount: number): number {
        if (this.isMemberShip){
            return amount;
        }
        return amount * 0.5;
    }
}

class DisCountService {
    constructor(private readonly discountStrategy: Discount) {
    }
    calculateDiscount(amount: number) {
        return this.discountStrategy.calculateDiscount(amount);
    }
}

const discount = new DisCountService( new StandardDiscount()); //Do not affect this code

Enter fullscreen mode Exit fullscreen mode

All the problems are fixed with OCP.


L - Liskov Substitution Principle (LSP)

Derived classes must be subtitutable for their base classes.

This means you should be able to replace a parent class with a subclass withou breaking functionality

❌Bad

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

    setWidth(width: number) {
        this.width = width;
    }

    setHeight(height: number) {
        this.height = height;
    }

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

class Square extends Rectangle {
    setWidth(width: number) {
        this.width = width;
        this.height = width; // A square's height is always equal to its width
    }

    setHeight(height: number) {
        this.height = height;
        this.width = height; // A square's width is always equal to its height
    }
}

// Usage
const shape: Rectangle = new Square(5, 5);
shape.setWidth(10);
console.log(shape.getArea()); // Expected: 50, Actual: 100

Enter fullscreen mode Exit fullscreen mode

why is it bad?

  • Square violates the bahavior expected of Retangle, Changing the width or height of a Square changes both dimensions, which is now how a Rectangle works.
  • Substituting a Square for a Rectangle lead to incorrect behavior:
const shape: Rectangle = new Square(5, 5); //Substituting a `Square` for a `Rectangle` lead to incorrect behavior
shape.setWidth(10);
Enter fullscreen mode Exit fullscreen mode

✅Good:

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

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

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

class Square extends Shape {
    constructor(protected side: number) {
        super();
    }

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

// Usage
const shape: Shape = new Rectangle(10, 5);
console.log(shape.getArea());
Enter fullscreen mode Exit fullscreen mode

Why is it better?

  • Shape defines a common getArea() method, and the both Rectangle and Square provide their specific implementations.
  • The Square class no longer inherits from Rectangle. This ensures that Square does not inherit setWidth() or setHeight() methods that don’t make sense for a square.

So what should I do apply LSP in my code correctly and efficially?
=> Avoid forcing inheritance when the subclass doesn’t fully align with the parent class's behavior.

[...Continue with next lession :)]

Top comments (0)