Understanding SOLID Principles in TypeScript
This article explains the five SOLID design principles—Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion—using clear TypeScript examples that demonstrate how to write cleaner, more maintainable code.
TypeScript greatly improves the ability to write clean JavaScript code, and applying the SOLID design principles invented by Robert C. Martin helps developers further improve code quality. The article introduces each principle with TypeScript examples hosted in a public GitHub repository.
Single Responsibility Principle (SRP)
"A class should have only one reason to change." By ensuring a class has a single purpose, maintenance becomes easier and side‑effects are minimized.
Bad example (a class that both models a book and saves it to a file):
class Book {
public title: string;
public author: string;
public description: string;
public pages: number;
// constructor and other methods
public saveToFile(): void {
// some fs.write method to save book to file
}
}Improved example that separates persistence from the domain model:
class Book {
public title: string;
public author: string;
public description: string;
public pages: number;
// constructor and other methods
}
class Persistence {
public saveToFile(book: Book): void {
// some fs.write method to save book to file
}
}Open/Closed Principle (OCP)
"Software entities should be open for extension but closed for modification."
Instead of modifying existing classes to add new functionality, extend them via abstractions such as interfaces.
Bad example: an AreaCalculator that must be changed each time a new shape is added.
class Rectangle {
public width: number;
public height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
}
class Circle {
public radius: number;
constructor(radius: number) {
this.radius = radius;
}
}
class AreaCalculator {
public calculateRectangleArea(rectangle: Rectangle): number {
return rectangle.width * rectangle.height;
}
public calculateCircleArea(circle: Circle): number {
return Math.PI * (circle.radius * circle.radius);
}
}Improved example using a Shape interface so the calculator works with any shape without modification:
interface Shape {
calculateArea(): number;
}
class Rectangle implements Shape {
public width: number;
public height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
public calculateArea(): number {
return this.width * this.height;
}
}
class Circle implements Shape {
public radius: number;
constructor(radius: number) {
this.radius = radius;
}
public calculateArea(): number {
return Math.PI * (this.radius * this.radius);
}
}
class AreaCalculator {
public calculateArea(shape: Shape): number {
return shape.calculateArea();
}
}Liskov Substitution Principle (LSP)
"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it."
A derived class should extend the base class without altering its expected behavior.
Bad example where Square overrides properties in a way that breaks the contract of Rectangle:
class Rectangle {
public width: number;
public height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
public calculateArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
public _width: number;
public _height: number;
constructor(width: number, height: number) {
super(width, height);
this._width = width;
this._height = height;
}
}Solution: remove the unnecessary subclass and keep a single Rectangle class with an additional isSquare method.
class Rectangle {
public width: number;
public height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
public calculateArea(): number {
return this.width * this.height;
}
public isSquare(): boolean {
return this.width === this.height;
}
}Interface Segregation Principle (ISP)
"Many client‑specific interfaces are better than one general‑purpose interface."
Prefer multiple small interfaces over a large, catch‑all interface.
Bad example: a Character interface that forces a Troll class to implement methods it never uses.
interface Character {
shoot(): void;
swim(): void;
talk(): void;
dance(): void;
}
class Troll implements Character {
public shoot(): void { /* ... */ }
public swim(): void { /* a troll can't swim */ }
public talk(): void { /* a troll can't talk */ }
public dance(): void { /* ... */ }
}Improved design splits the responsibilities into four focused interfaces and lets Troll implement only the ones it needs.
interface Talker { talk(): void; }
interface Shooter { shoot(): void; }
interface Swimmer { swim(): void; }
interface Dancer { dance(): void; }
class Troll implements Shooter, Dancer {
public shoot(): void { /* ... */ }
public dance(): void { /* ... */ }
}Dependency Inversion Principle (DIP)
"Depend on abstractions, not on concretions."
High‑level modules should not depend on low‑level modules; both should depend on abstractions.
Bad example: SoftwareProject directly creates FrontendDeveloper and BackendDeveloper instances.
class FrontendDeveloper {
public writeHtmlCode(): void { /* ... */ }
}
class BackendDeveloper {
public writeTypeScriptCode(): void { /* ... */ }
}
class SoftwareProject {
public frontendDeveloper: FrontendDeveloper;
public backendDeveloper: BackendDeveloper;
constructor() {
this.frontendDeveloper = new FrontendDeveloper();
this.backendDeveloper = new BackendDeveloper();
}
public createProject(): void {
this.frontendDeveloper.writeHtmlCode();
this.backendDeveloper.writeTypeScriptCode();
}
}Improved design introduces a Developer abstraction and lets the project work with any developers that implement it.
interface Developer { develop(): void; }
class FrontendDeveloper implements Developer {
public develop(): void { this.writeHtmlCode(); }
private writeHtmlCode(): void { /* ... */ }
}
class BackendDeveloper implements Developer {
public develop(): void { this.writeTypeScriptCode(); }
private writeTypeScriptCode(): void { /* ... */ }
}
class SoftwareProject {
public developers: Developer[];
public createProject(): void {
this.developers.forEach((developer: Developer) => developer.develop());
}
}The article concludes by encouraging readers to explore more about SOLID on Wikipedia and provides links to additional resources.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architects Research Society
A daily treasure trove for architects, expanding your view and depth. We share enterprise, business, application, data, technology, and security architecture, discuss frameworks, planning, governance, standards, and implementation, and explore emerging styles such as microservices, event‑driven, micro‑frontend, big data, data warehousing, IoT, and AI architecture.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
