Design Patterns in TypeScript: Examples and Application

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

Design Patterns in TypeScript: A Practical Guide



Design patterns are proven solutions to common problems in software development. In TypeScript, thanks to strict typing and OOP capabilities, many patterns are implemented particularly elegantly and safely. In this article, we will examine four key patterns with real code examples that will help you write more flexible and maintainable code.



1. Singleton



The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. In TypeScript, this is especially useful for managing application configuration or database connections.



Implementation using a private constructor and a static method:



class DatabaseConnection {  private static instance: DatabaseConnection;  private connection: string;

private constructor() { this.connection = 'PostgreSQL connected'; }

public static getInstance(): DatabaseConnection { if (!DatabaseConnection.instance) { DatabaseConnection.instance = new DatabaseConnection(); } return DatabaseConnection.instance; }

public query(sql: string): void { console.log(`Executing query: ${sql} on ${this.connection}`); }}

// Usageconst db1 = DatabaseConnection.getInstance();const db2 = DatabaseConnection.getInstance();console.log(db1 === db2); // true - same objectdb1.query('SELECT * FROM users');


When to use: for loggers, caches, thread pools, application settings. Avoid Singleton for states that may change in a multithreaded environment.



2. Factory Method



The Factory Method defines an interface for creating an object but allows subclasses to alter the type of object that will be created. In TypeScript, this works well with interfaces and abstract classes.



Example of creating different types of notifications:



interface Notification {  send(message: string): void;}

class EmailNotification implements Notification { public send(message: string): void { console.log(`Email sent: ${message}`); }}

class SMSNotification implements Notification { public send(message: string): void { console.log(`SMS sent: ${message}`); }}

abstract class NotificationFactory { public abstract createNotification(): Notification;

public notifyUser(message: string): void { const notification = this.createNotification(); notification.send(message); }}

class EmailNotificationFactory extends NotificationFactory { public createNotification(): Notification { return new EmailNotification(); }}

class SMSNotificationFactory extends NotificationFactory { public createNotification(): Notification { return new SMSNotification(); }}

// Usageconst factory: NotificationFactory = new EmailNotificationFactory();factory.notifyUser('Your order has been shipped!');


When to use: when it is not known in advance which specific class will be needed, or when the object creation logic is complex and should be centralized.



3. Observer



The Observer defines a one-to-many relationship between objects: when the state of one object changes, all dependent objects are notified automatically. In TypeScript, this is often implemented using generics for event typing.



Example of a news subscription system:



interface Observer<T> {  update(data: T): void;}

class NewsPublisher<T> { private observers: Observer<T>[] = [];

public subscribe(observer: Observer<T>): void { this.observers.push(observer); }

public unsubscribe(observer: Observer<T>): void { this.observers = this.observers.filter(obs => o

Blogs

Book Recommendations