SOLID Principles in TypeScript: Practical Examples
SOLID principles guide you toward flexible, maintainable object-oriented code.
Single Responsibility Principle (SRP)
A class should have only one reason to change.
// Bad: handles both user data and email sending
class UserService {
createUser(name: string, email: string) { /* ... */ }
sendWelcomeEmail(email: string) { /* ... */ }
generateReport() { /* ... */ }
}
// Good: separate responsibilities
class UserRepository {
create(user: User): Promise<User> { /* ... */ }
findById(id: string): Promise<User> { /* ... */ }
}
class EmailService {
sendWelcome(email: string): Promise<void> { /* ... */ }
}
class UserReportGenerator {
generate(): Report { /* ... */ }
}
Open/Closed Principle (OCP)
Open for extension, closed for modification.
// Bad: modify class to add discount types
class OrderCalculator {
calculate(order: Order): number {
if (order.type === 'regular') return order.total;
if (order.type === 'premium') return order.total * 0.9;
// Must modify class for new types
}
}
// Good: extend without modifying
interface DiscountStrategy {
apply(total: number): number;
}
class RegularDiscount implements DiscountStrategy {
apply(total: number) { return total; }
}
class PremiumDiscount implements DiscountStrategy {
apply(total: number) { return total * 0.9; }
}
class BlackFridayDiscount implements DiscountStrategy {
apply(total: number) { return total * 0.7; }
}
class OrderCalculator {
constructor(private discount: DiscountStrategy) {}
calculate(order: Order): number {
return this.discount.apply(order.total);
}
}
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types.
// Bad: Square breaks Rectangle contract
class Rectangle {
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
area() { return this.width * this.height; }
}
class Square extends Rectangle {
setWidth(w: number) { this.width = w; this.height = w; } // Breaks LSP!
}
// Good: separate hierarchy
interface Shape {
area(): number;
}
class Rectangle implements Shape { /* ... */ }
class Square implements Shape { /* ... */ }
Interface Segregation Principle (ISP)
Clients should not depend on interfaces they don't use.
// Bad: fat interface
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
// Good: small, focused interfaces
interface Workable { work(): void; }
interface Eatable { eat(): void; }
interface Sleepable { sleep(): void; }
class Human implements Workable, Eatable, Sleepable { /* ... */ }
class Robot implements Workable { /* only work */ }
Dependency Inversion Principle (DIP)
Depend on abstractions, not concretions.
// Bad: depends on concrete implementation
class OrderService {
private db = new MySQLDatabase(); // hard dependency
saveOrder(order: Order) { this.db.save(order); }
}
// Good: depends on abstraction
interface Database {
save<T>(data: T): Promise<void>;
find<T>(id: string): Promise<T>;
}
class OrderService {
constructor(private db: Database) {} // injected
async saveOrder(order: Order) { await this.db.save(order); }
}
// Compose in application root
const service = new OrderService(new PostgresDatabase());
SOLID principles make systems easier to test, extend, and maintain.