Skip to content

Design Patterns in TypeScript

Published: at 10:00 AMSuggest Changes

Design patterns are proven solutions to common problems in software design. They provide a template for writing code that is easy to understand, maintain, and extend. Let’s explore five essential design patterns in TypeScript: Singleton, Factory, Observer, Strategy, and Decorator.

1. Singleton Pattern

What is it?

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

Implementation

class Logger {
  private static instance: Logger;

  private constructor() {}

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

  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }
}

const loggerA = Logger.getInstance();
const loggerB = Logger.getInstance();
loggerA.log("Singleton pattern example");
console.log(loggerA === loggerB); // true

Pros and Cons

Pros:

  • Controlled access to the single instance.
  • Reduces the need for global variables.

Cons:

  • Can make unit testing tricky.
  • May introduce hidden dependencies.

2. Factory Pattern

What is it?

The Factory pattern provides a way to create objects without specifying the exact class of object that will be created.

Implementation

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}`);
  }
}

class NotificationFactory {
  public static createNotification(type: string): Notification {
    switch (type) {
      case "email":
        return new EmailNotification();
      case "sms":
        return new SMSNotification();
      default:
        throw new Error("Unknown notification type");
    }
  }
}

const emailNotification = NotificationFactory.createNotification("email");
emailNotification.send("Hello via Email!");

const smsNotification = NotificationFactory.createNotification("sms");
smsNotification.send("Hello via SMS!");

Pros and Cons

Pros:

  • Encapsulates object creation logic.
  • Makes it easier to add new types of products.

Cons:

  • Can introduce complexity for simple object creation.

3. Observer Pattern

What is it?

The Observer pattern allows an object (subject) to notify other objects (observers) about changes in its state.

Implementation

class WeatherStation {
  private temperature: number = 0;
  private observers: Observer[] = [];

  addObserver(observer: Observer): void {
    this.observers.push(observer);
  }

  removeObserver(observer: Observer): void {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  setTemperature(temp: number): void {
    console.log(`WeatherStation: new temperature measurement: ${temp}`);
    this.temperature = temp;
    this.notifyObservers();
  }

  private notifyObservers(): void {
    for (const observer of this.observers) {
      observer.update(this.temperature);
    }
  }
}

interface Observer {
  update(temperature: number): void;
}

class TemperatureDisplay implements Observer {
  update(temperature: number): void {
    console.log(
      `TemperatureDisplay: I need to update my display to ${temperature}`
    );
  }
}

const weatherStation = new WeatherStation();
const tempDisplay = new TemperatureDisplay();

weatherStation.addObserver(tempDisplay);
weatherStation.setTemperature(25);
weatherStation.setTemperature(30);

Pros and Cons

Pros:

  • Promotes loose coupling.
  • Supports broadcast communication.

Cons:

  • Can be complex to manage with many observers.

4. Strategy Pattern

What is it?

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

Implementation

class PaymentContext {
  private strategy: PaymentStrategy;

  setStrategy(strategy: PaymentStrategy): void {
    this.strategy = strategy;
  }

  executeStrategy(amount: number): void {
    this.strategy.pay(amount);
  }
}

interface PaymentStrategy {
  pay(amount: number): void;
}

class CreditCardPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Paid ${amount} using Credit Card`);
  }
}

class PayPalPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Paid ${amount} using PayPal`);
  }
}

const paymentContext = new PaymentContext();
paymentContext.setStrategy(new CreditCardPayment());
paymentContext.executeStrategy(100); // Paid 100 using Credit Card

paymentContext.setStrategy(new PayPalPayment());
paymentContext.executeStrategy(200); // Paid 200 using PayPal

Pros and Cons

Pros:

  • Allows easy switching of algorithms.
  • Promotes open/closed principle.

Cons:

  • Increases the number of classes.

5. Decorator Pattern

What is it?

The Decorator pattern allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class.

Implementation

interface Coffee {
  cost(): number;
  description(): string;
}

class SimpleCoffee implements Coffee {
  cost(): number {
    return 5;
  }

  description(): string {
    return "Simple coffee";
  }
}

class MilkDecorator implements Coffee {
  constructor(private coffee: Coffee) {}

  cost(): number {
    return this.coffee.cost() + 2;
  }

  description(): string {
    return `${this.coffee.description()} with milk`;
  }
}

class SugarDecorator implements Coffee {
  constructor(private coffee: Coffee) {}

  cost(): number {
    return this.coffee.cost() + 1;
  }

  description(): string {
    return `${this.coffee.description()} with sugar`;
  }
}

let coffee: Coffee = new SimpleCoffee();
console.log(`${coffee.description()} costs $${coffee.cost()}`);

coffee = new MilkDecorator(coffee);
console.log(`${coffee.description()} costs $${coffee.cost()}`);

coffee = new SugarDecorator(coffee);
console.log(`${coffee.description()} costs $${coffee.cost()}`);

Pros and Cons

Pros:

  • Flexible alternative to subclassing.
  • Can add responsibilities to objects dynamically.

Cons:

  • Can lead to a large number of small classes.

Conclusion

Design patterns are essential tools in a developer’s toolkit. They provide solutions to common problems and help create code that is maintainable and scalable. Understanding and applying these patterns in TypeScript improve the quality of the codebase.

Further Reading