SOLID Principles in TypeScript: Practical Examples
SOLID principles guide you toward maintainable, flexible object-oriented design.
Single Responsibility Principle (SRP)
Each class should have one reason to change.
// Bad: UserService does too many things
class UserService {
saveUser(user: User) { /* DB logic */ }
sendWelcomeEmail(user: User) { /* Email logic */ }
generateReport(userId: string) { /* Report logic */ }
}
// Good: Separate concerns
class UserRepository {
async save(user: User): Promise<void> { /* DB logic */ }
async findById(id: string): Promise<User | null> { /* DB logic */ }
}
class EmailService {
async sendWelcome(user: User): Promise<void> { /* Email logic */ }
}
class UserReportService {
async generate(userId: string): Promise<Report> { /* Report logic */ }
}
Open/Closed Principle (OCP)
Open for extension, closed for modification.
// Bad: Adding new discount type requires modifying existing code
class DiscountCalculator {
calculate(type: string, price: number): number {
if (type === 'seasonal') return price * 0.9;
if (type === 'member') return price * 0.85;
// Every new type requires modifying this class
return price;
}
}
// Good: Extend via new implementations
interface DiscountStrategy {
apply(price: number): number;
}
class SeasonalDiscount implements DiscountStrategy {
apply(price: number): number { return price * 0.9; }
}
class MemberDiscount implements DiscountStrategy {
apply(price: number): number { return price * 0.85; }
}
class PriceCalculator {
constructor(private discount: DiscountStrategy) {}
calculate(price: number): number {
return this.discount.apply(price);
}
}
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types.
// Bad: Square breaks Rectangle behavior
class Rectangle {
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
area(): number { return this.width * this.height; }
protected width = 0;
protected height = 0;
}
class Square extends Rectangle {
setWidth(w: number) { this.width = this.height = w; } // violates LSP!
setHeight(h: number) { this.width = this.height = h; }
}
// Good: Separate abstractions
interface Shape {
area(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area(): number { return this.width * this.height; }
}
class Square implements Shape {
constructor(private side: number) {}
area(): number { return this.side ** 2; }
}
Interface Segregation Principle (ISP)
Don't force clients to depend on interfaces they don't use.
// Bad: Fat interface
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
// Good: Segregated interfaces
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
class HumanWorker implements Workable, Eatable, Sleepable {
work() { /* ... */ }
eat() { /* ... */ }
sleep() { /* ... */ }
}
class Robot implements Workable {
work() { /* ... */ }
// Robots don't need eat/sleep
}
Dependency Inversion Principle (DIP)
Depend on abstractions, not concretions.
// Bad: High-level depends on low-level
class OrderService {
private db = new MySQLDatabase(); // concrete dependency!
async createOrder(order: Order): Promise<void> {
await this.db.insert('orders', order);
}
}
// Good: Depend on abstraction
interface Database {
insert(table: string, data: unknown): Promise<void>;
query<T>(sql: string, params: unknown[]): Promise<T[]>;
}
class OrderService {
constructor(private db: Database) {} // inject abstraction
async createOrder(order: Order): Promise<void> {
await this.db.insert('orders', order);
}
}
// Wire up in composition root
const db = new PostgreSQLDatabase(config);
const orderService = new OrderService(db);
SOLID principles work together to create systems that are easy to test, extend, and maintain.