Introduction: Why SOLID is Important for a TypeScript Developer
SOLID is a set of five fundamental principles of object-oriented programming formulated by Robert Martin (Uncle Bob). In the context of TypeScript, which combines static typing and the flexibility of JavaScript, applying SOLID principles allows you to create scalable, testable, and easily maintainable applications. In this article, we will break down each principle with a theoretical foundation and practical examples in TypeScript.
1. Single Responsibility Principle (SRP)
Theory
A class should have only one reason to change. This means that each module, class, or function is responsible for exactly one piece of functionality.
Practice in TypeScript
Consider a violation of SRP using the example of a class that manages users and sends notifications:
// ❌ SRP Violationclass UserManager { constructor(private name: string, private email: string) {}
saveUser(): void { console.log(`User ${this.name} saved to the database`); }
sendWelcomeEmail(): void { console.log(`Welcome email sent to ${this.email}`); }}Corrected version with separation of concerns:
// ✅ SRP Complianceclass User { constructor(public name: string, public email: string) {}}
class UserRepository { save(user: User): void { console.log(`User ${user.name} saved to the database`); }}
class EmailService { sendWelcomeEmail(user: User): void { console.log(`Welcome email sent to ${user.email}`); }}2. Open/Closed Principle (OCP)
Theory
Software entities should be open for extension but closed for modification. In other words, you should be able to add new functionality without changing existing code.
Practice in TypeScript
Example with discount calculation for different customer types:
// ❌ OCP Violationclass DiscountCalculator { calculateDiscount(customerType: string, amount: number): number { if (customerType === 'regular') { return amount * 0.1; } else if (customerType === 'premium') { return amount * 0.2; } else if (customerType === 'vip') { return amount * 0.3; } return 0; }}// ✅ OCP Compliance via Polymorphisminterface DiscountStrategy { calculate(amount: number): number;}
class RegularDiscount implements DiscountStrategy { calculate(amount: number): number { return amount * 0.1; }}
class PremiumDiscount implements DiscountStrategy { calculate(amount: number): number { return amount * 0.2; }}
class DiscountCalculator { constructor(private strategy: DiscountStrategy) {}
calculate(amount: number): number { return this.strategy.calculate(amount); }}3. Liskov Substitution Principle (LSP)
Theory
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program. Simply put: if you have a base class, any of its subclasses should work correctly wherever the base class is used.
Practice in TypeScript
Classic example with geometric shapes:
// ❌ LSP Violationclass Rectangle { constructor(protected width: number, protected height: number) {}
setWidth(width: number): void { this.width = width; }
setHeight(height: number): void { this.height = height; }
getArea(): number { return this.width * this.height; }}
class Square extends Rectangle { setWidth(width: number): void { this.width = width; this.height = width; // Violation! }
setHeight(height: number): vo