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 to show how each principle improves code maintainability and extensibility.
TypeScript greatly influences writing clean JavaScript code, and following the SOLID design principles—originated by Robert C. Martin—helps developers improve code quality and maintainability.
Single Responsibility Principle (SRP) : A class should have only one reason to change. The article shows a bad example where a Book class handles both data and file persistence, and a refactored version that separates persistence into a dedicated class.
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
}
} 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. The initial AreaCalculator class requires changes whenever a new shape is added. By introducing a Shape interface, new shapes can be added without altering the calculator.
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);
}
} 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) : Subtypes must be substitutable for their base types without altering program correctness. The example shows a Square class that inherits from Rectangle but overrides properties unnecessarily; removing the subclass and handling squares within Rectangle respects LSP.
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;
}
} 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) : Clients should not be forced to depend on interfaces they do not use. The article replaces a monolithic Character interface with four fine‑grained interfaces, allowing a Troll class to implement only the behaviors it actually needs.
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 { /* ... */ }
} 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) : High‑level modules should depend on abstractions, not concrete implementations. The original SoftwareProject directly creates FrontendDeveloper and BackendDeveloper. By introducing a Developer interface and injecting a collection of developers, the high‑level project class depends only on the abstraction.
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();
}
} 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 the accompanying GitHub repository for the full source code and to apply these principles in their own TypeScript projects.
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.
