Design Patterns in TypeScript: From Simple to Complex
Design patterns are time-tested solutions to typical problems in software development. In the context of TypeScript, which adds strict typing and OOP capabilities to JavaScript, patterns become especially effective. TypeScript allows you to implement abstractions, interfaces, and polymorphism at a new level, making code more predictable and safe.
In this article, we will cover key design patterns, their implementation in TypeScript, and practical usage examples. The material will be useful for both beginner developers and experienced professionals looking to systematize their knowledge.
Creational Patterns: Singleton and Factory Method
Creational patterns are responsible for creating objects, hiding the instantiation logic. Let's look at two of the most popular ones: Singleton and Factory Method.
Singleton
Singleton guarantees that a class has only one instance and provides a global access point to it. In TypeScript, the implementation becomes especially elegant thanks to static members and private constructors.
class DatabaseConnection { private static instance: DatabaseConnection; private connectionString: string;
private constructor(connectionString: string) { this.connectionString = connectionString; console.log(`Connection created to: ${connectionString}`); }
static getInstance(connectionString: string): DatabaseConnection { if (!DatabaseConnection.instance) { DatabaseConnection.instance = new DatabaseConnection(connectionString); } return DatabaseConnection.instance; }
query(sql: string): void { console.log(`Executing query: ${sql} on ${this.connectionString}`); }}
// Usageconst db1 = DatabaseConnection.getInstance('postgres://localhost:5432/mydb');const db2 = DatabaseConnection.getInstance('postgres://localhost:5432/mydb');console.log(db1 === db2); // true - the same objectThis pattern is ideal for managing resources: connection pools, logging, caching. However, be careful — Singleton can complicate testing due to global state.
Factory Method
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 is implemented through abstract classes or interfaces.
interface Notification { send(message: string): void;}
class EmailNotification implements Notification { send(message: string): void { console.log(`Sending email: ${message}`); }}
class SMSNotification implements Notification { send(message: string): void { console.log(`Sending SMS: ${message}`); }}
abstract class NotificationFactory { abstract createNotification(): Notification;
notify(message: string): void { const notification = this.createNotification(); notification.send(message); }}
class EmailFactory extends NotificationFactory { createNotification(): Notification { return new EmailNotification(); }}
class SMSFactory extends NotificationFactory { createNotification(): Notification { return new SMSNotification(); }}
// Usageconst factory: NotificationFactory = new EmailFactory();factory.notify('Your order has been confirmed!');Factory Method helps adhere to the Open/Closed Principle (OCP) — you can add new notification types without changing existing code.
Structural Patterns: Adapter and Decorator
Structural patterns help organize relationships between objects and classes, making the system more flexible and extensible.