SOLID Principles in TypeScript: Theory and Practice from A to Z

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

SOLID Principles in TypeScript: Theory and Practice from A to Z

\n\n

SOLID is an acronym for five fundamental principles of object-oriented programming, formulated by Robert Martin (Uncle Bob). These principles help create flexible, maintainable, and scalable code. In this article, we will thoroughly examine each SOLID principle with examples in TypeScript, from simple definitions to real-world usage scenarios.

\n\n

1. Single Responsibility Principle (SRP)

\n\n

Definition: Each class should have only one reason to change. In other words, a class should be responsible for one clearly defined task.

\n\n

Problem: When a class takes on multiple responsibilities, it becomes difficult to test, extend, and maintain. Changing one function can break another.

\n\n

Example of SRP violation:

\n
// ❌ SRP violation: the class handles storage, formatting, and sending\nclass UserService {\n    saveUser(user: User): void {\n        // Saving to DB\n        console.log('User saved');\n    }\n\n    formatUserData(user: User): string {\n        return `Name: ${user.name}, Email: ${user.email}`;\n    }\n\n    sendWelcomeEmail(user: User): void {\n        // Sending email\n        console.log('Email sent');\n    }\n}
\n\n

Solution with SRP:

\n
// ✅ Each class is responsible for its own task\nclass UserRepository {\n    save(user: User): void {\n        console.log('User saved');\n    }\n}\n\nclass UserFormatter {\n    format(user: User): string {\n        return `Name: ${user.name}, Email: ${user.email}`;\n    }\n}\n\nclass EmailService {\n    sendWelcome(user: User): void {\n        console.log('Email sent');\n    }\n}
\n\n

2. Open-Closed Principle (OCP)

\n\n

Definition: Classes should be open for extension but closed for modification. This means you should be able to add new functionality without changing existing code.

\n\n

Example of OCP violation:

\n
// ❌ When adding a new shape type, you have to modify the calculateArea method\nclass AreaCalculator {\n    calculateArea(shape: any): number {\n        if (shape.type === 'circle') {\n            return Math.PI * shape.radius ** 2;\n        } else if (shape.type === 'rectangle') {\n            return shape.width * shape.height;\n        }\n        throw new Error('Unknown shape');\n    }\n}
\n\n

Solution with OCP (using polymorphism):

\n
// ✅ Using interface and inheritance\ninterface Shape {\n    area(): number;\n}\n\nclass Circle implements Shape {\n    constructor(private radius: number) {}\n    area(): number {\n        return Math.PI * this.radius ** 2;\n    }\n}\n\nclass Rectangle implements Shape {\n    constructor(private width: number, private height: number) {}\n    area(): number {\n        return this.width * this.height;\n    }\n}\n\nclass AreaCalculator {\n    calculateArea(shape: Shape): number {\n        return shape.area(); // No changes required when adding new shapes\n    }\n}
\n\n

3. Liskov Substitution Principle (LSP)

\n\n

Definition: Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program. A derived class must not violate the contract of the parent class.

\n\n

Example of LSP violation:

\n
// ❌ Violation: the subclass changes the behavior of the parent\nclass Bird {\n    fly(): void {\n        console.log('Flying');\n    }\n}\n\nclass Penguin extends Bird {\n    fly(): void {\n        throw new Error('Penguins cannot fly!'); // Contract violation\n    }\n}
\n\n

Solution with LSP:

\n
// ✅ Separated                

Blogs

Book Recommendations