Introduction: Why SOLID is Important in TypeScript
SOLID is an acronym for five fundamental principles of object-oriented design formulated by Robert Martin (Uncle Bob). In the context of TypeScript, these principles gain particular value thanks to the strict type system and support for modern OOP paradigms. Applying SOLID makes code more flexible, testable, and maintainable, which is critically important for scalable projects.
In this article, we will break down each SOLID principle using practical TypeScript examples, show antipatterns and their correct implementations. You will learn how to write code that doesn't break when adding new functionality.
1. Single Responsibility Principle (SRP)
Theory
Each class should have only one reason to change. This means that a class should be responsible for only one part of the program's functionality. If a class performs multiple tasks, it is harder to maintain and test.
Antipattern (SRP violation)
class UserService { saveUser(user: User): void { // Saving to DB console.log('User saved'); }
sendEmail(user: User, message: string): void { // Sending email console.log(`Email sent to ${user.email}`); }
generateReport(user: User): string { // Generating report return `Report for ${user.name}`; }}Correct implementation (SRP)
class UserRepository { save(user: User): void { // Only saving }}
class EmailService { send(user: User, message: string): void { // Only sending email }}
class ReportGenerator { generate(user: User): string { // Only generating reports }}2. Open-Closed Principle (OCP)
Theory
Classes should be open for extension but closed for modification. New functionality should be added through inheritance or composition, not by changing existing code.
Antipattern (OCP violation)
class PaymentProcessor { processPayment(type: string, amount: number): void { if (type === 'card') { // Card processing } else if (type === 'paypal') { // PayPal processing } else if (type === 'crypto') { // Cryptocurrency processing } // When adding a new type — you need to change the class! }}Correct implementation (OCP)
interface PaymentMethod { process(amount: number): void;}
class CardPayment implements PaymentMethod { process(amount: number): void { console.log(`Processing card payment: $${amount}`); }}
class PayPalPayment implements PaymentMethod { process(amount: number): void { console.log(`Processing PayPal payment: $${amount}`); }}
class PaymentProcessor { constructor(private paymentMethod: PaymentMethod) {}
process(amount: number): void { this.paymentMethod.process(amount); }}3. Liskov Substitution Principle (LSP)
Theory
Objects of a base class should be replaceable with objects of its subclasses without breaking the program. Subclasses should not alter the expected behavior of the base class.
Antipattern (LSP violation)
class Rectangle { constructor(public width: number, public 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 {