SOLID Principles in TypeScript: Theory and Practice from A to Z
\n\nSOLID 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\n1. Single Responsibility Principle (SRP)
\n\nDefinition: Each class should have only one reason to change. In other words, a class should be responsible for one clearly defined task.
\n\nProblem: When a class takes on multiple responsibilities, it becomes difficult to test, extend, and maintain. Changing one function can break another.
\n\nExample 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\nSolution 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\n2. Open-Closed Principle (OCP)
\n\nDefinition: 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\nExample 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\nSolution 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\n3. Liskov Substitution Principle (LSP)
\n\nDefinition: 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\nExample 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\nSolution with LSP:
\n// ✅ Separated