Typescript Design Patterns

triyanox

Mohamed Achaq

Posted on May 1, 2022

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.

💖 💪 🙅 🚩
triyanox
Mohamed Achaq

Posted on May 1, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024

How to Use KitOps with MLflow
beginners How to Use KitOps with MLflow

November 29, 2024