SOLID Principles in TypeScript: Theory and Practice from A to Z

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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

Blogs

Book Recommendations