DEV Community

Cover image for Typescript Design Patterns
Mohamed Achaq
Mohamed Achaq

Posted on • Edited on

Typescript Design Patterns

Design patterns are a set of best practices used to solve common problems in software development and make writing clean and maintainable code easier. In this blog post, we will discuss some of the most commonly used design patterns in TypeScript.

Singleton

The Singleton pattern is a design pattern that restricts the instantiation of a class to one object and ensures that only one object of a class is created. Implementing the Singleton pattern in TypeScript is very easy:

class Singleton {
  private static instance: Singleton;

  private constructor() {}

  public static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}
Enter fullscreen mode Exit fullscreen mode

You can use it like this:

const singleton = Singleton.getInstance();
Enter fullscreen mode Exit fullscreen mode

Factory

The Factory pattern is a design pattern that lets you create objects without specifying the exact class of the object that will be created. In this example, we want to make a vehicle depending on its type, so instead of making a class for each type, we make a single factory class to make us a vehicle depending on the type we give it.

abstract class Vehicle {
  abstract getType(): string;
}

class Car extends Vehicle {
  getType() {
    return 'car';
  }
}

class Truck extends Vehicle {
  getType() {
    return 'truck';
  }
}

class VehicleFactory {
  public createVehicle(type: string): Vehicle {
    switch (type) {
      case 'car':
        return new Car();
      case 'truck':
        return new Truck();
      default:
        throw new Error(`Vehicle of type ${type} not found`);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You can use it like this to make as many vehicles as you want, as long as you provide the type:

const factory = new VehicleFactory();
const car = factory.createVehicle('car');
const truck = factory.createVehicle('truck');
Enter fullscreen mode Exit fullscreen mode

Observer

The Observer pattern is a design pattern that lets you define a subscription mechanism to notify multiple objects and is used in the event-driven programming paradigm. Implementing the Observer pattern in TypeScript can look like this:

interface Observer {
  update(data: any): void;
}

class Subject {
  private observers: Observer[] = [];

  public subscribe(observer: Observer) {
    this.observers.push(observer);
  }

  public unsubscribe(observer: Observer) {
    const index = this.observers.indexOf(observer);
    this.observers.splice(index, 1);
  }

  public notify(data: any) {
    this.observers.forEach(observer => observer.update(data));
  }
}
Enter fullscreen mode Exit fullscreen mode

Then you will need an observer class:

class ConcreteObserver implements Observer {
  public update(data: any) {
    console.log(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

You can let the subject know that there is new data available by subscribing to the observer we created:

const subject = new Subject();
const observer = new ConcreteObserver();
subject.subscribe(observer);
subject.notify('Hello World');

// Unsubscribe the observer from the subject:
subject.unsubscribe(observer);
Enter fullscreen mode Exit fullscreen mode

Command

The Command pattern is a design pattern that lets you encapsulate all information needed to perform an action in one object. Implementing the Command pattern can look like this:

interface Command {
  execute(): void;
}

class ConcreteCommand implements Command {
  constructor(private receiver: Receiver) {}

  public execute() {
    this.receiver.action();
  }
}

class Receiver {
  public action() {
    console.log('Action called');
  }
}
Enter fullscreen mode Exit fullscreen mode

Then you can use the command module to create a command object and pass it to the invoker:

const receiver = new Receiver();
const command = new ConcreteCommand(receiver);
const invoker = new Invoker();
invoker.setCommand(command);
invoker.execute();
Enter fullscreen mode Exit fullscreen mode

Strategy

The Strategy pattern is a design pattern that lets you define a family of algorithms, encapsulate each one, and make them interchangeable. Implementing the Strategy pattern in TypeScript is very easy and you can start with this Strategy class:

interface Strategy {
  execute(data: any): any;
}

class LastElementStrategy implements Strategy {
  public execute(data: []) {
    return data[data.length - 1];
  }
}
Enter fullscreen mode Exit fullscreen mode

Then you can use it like this:

const strategy = new LastElementStrategy();
const data = [1, 2, 3, 4, 5];

let last = strategy.execute(data);
Enter fullscreen mode Exit fullscreen mode

Template Method

The Template Method pattern is a design pattern that lets you define the skeleton of an algorithm in an operation, deferring some steps to subclasses. For example, you want to make a pizza and you want to make it with tomato sauce and cheese, but the toppings can be different. You can use the Template Method pattern like this:

abstract class Pizza {
  public makePizza() {
    this.prepareDough();
    this.addTomatoSauce();
    this.addCheese();
    this.addToppings();
    this.bakePizza();
  }

  protected prepareDough() {
    console.log('Preparing dough');
  }

  protected addTomatoSauce() {
    console.log('Adding tomato sauce');
  }

  protected addCheese() {
    console.log('Adding cheese');
  }

  protected abstract addToppings(): void;

  protected bakePizza() {
    console.log('Baking pizza');
  }
}

class PepperoniPizza extends Pizza {
  protected addToppings() {
    console.log('Adding pepperoni');
  }
}

class VegetarianPizza extends Pizza {
  protected addToppings() {
    console.log('Adding vegetables');
  }
}
Enter fullscreen mode Exit fullscreen mode

You can use it like this to make both types of pizza:

const pepperoniPizza = new PepperoniPizza();
pepperoniPizza.makePizza();

const vegetarianPizza = new VegetarianPizza();
vegetarianPizza.makePizza();
Enter fullscreen mode Exit fullscreen mode

Conclusion

These are just a few examples of design patterns that can be used in TypeScript. By using these patterns, you can write better code that is easier to maintain and extend. It's important to note that design patterns should not be used blindly, but rather as tools to solve specific problems in your code.

Top comments (5)

Collapse
 
billsourour profile image
Bill Sourour

Thank you for taking the time to put together some examples in TypeScript.

For readers interested in learning more, these patterns are part of what is often called the "Gang of Four" patterns and they come from a book called Design Patterns: Elements of Reusable Object-Oriented Software (1994) written by the "Gang of Four"; Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.

It's important to not only know the patterns but also to understand what problems they solve so that you can make good decisions about which patterns to use when.

Collapse
 
triyanox profile image
Mohamed Achaq

Thank you so much and I really recommend this book too to anyone wants to dive deeper and understand design patterns !

Collapse
 
andresbecker profile image
Andres Becker

Thanks!

Collapse
 
andrewbaisden profile image
Andrew Baisden

Great read thanks for putting this together.

Collapse
 
triyanox profile image
Mohamed Achaq

Thank you so much really appreciated !