SOLID Principles in TypeScript: Advanced Theory and Practice
SOLID is a set of five fundamental principles of object-oriented design that allow you to create flexible, maintainable, and scalable code. In TypeScript, thanks to strong typing, interfaces, and abstract classes, implementing these principles becomes especially effective. In this article, we will break down each principle at an advanced level with real-world TypeScript examples.
1. Single Responsibility Principle (SRP)
Theory: Each class should have only one reason to change. In TypeScript, this means a module is responsible for exactly one functionality.
Advanced Practice: Violation of SRP often manifests in "God Objects." Use composition and delegation.
// ❌ SRP violation: the class handles data, validation, and persistenceclass UserService { constructor(private user: User) {}
validateEmail(email: string): boolean { return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email); }
saveToDatabase(): void { // saving logic }
sendWelcomeEmail(): void { // sending logic }}
// ✅ SRP: separate responsibilitiesinterface IEmailValidator { isValid(email: string): boolean;}
interface IUserRepository { save(user: User): Promise<void>;}
interface INotificationService { sendWelcome(user: User): Promise<void>;}
class UserRegistrationService { constructor( private validator: IEmailValidator, private repository: IUserRepository, private notifier: INotificationService ) {}
async register(user: User): Promise<void> { if (!this.validator.isValid(user.email)) { throw new Error('Invalid email'); } await this.repository.save(user); await this.notifier.sendWelcome(user); }}2. Open/Closed Principle (OCP)
Theory: Classes should be open for extension but closed for modification. In TypeScript, this is achieved through abstract classes and interfaces.
Advanced Practice: Use the Strategy pattern to implement OCP.
// ❌ OCP violation: adding a new report type requires modifying the classclass ReportGenerator { generate(data: any[], type: string): string { if (type === 'csv') { return data.map(row => row.join(',')).join(''); } else if (type === 'json') { return JSON.stringify(data); } // adding 'xml' — we modify this method throw new Error('Unsupported format'); }}
// ✅ OCP: use polymorphisminterface IReportFormatter { format(data: any[]): string;}
class CsvFormatter implements IReportFormatter { format(data: any[]): string { return data.map(row => row.join(',')).join(''); }}
class JsonFormatter implements IReportFormatter { format(data: any[]): string { return JSON.stringify(data); }}
class XmlFormatter implements IReportFormatter { format(data: any[]): string { // XML implementation return '<data>...</data>'; }}
class ReportGeneratorOCP { constructor(private formatter: IReportFormatter) {}
generate(data: any[]): string { return this.formatter.format(data); }}
// Usage: extend without modificationconst csvReport = new ReportGeneratorOCP(new CsvFormatter());const jsonReport = new ReportGeneratorOCP(new JsonFormatter());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. In TypeScript, this is especially critical due to nominal typing.