Top 5 Most Used Patterns in OOP with TypeScript

Photo of Volodymyr Voleniuk

Volodymyr Voleniuk

Updated Dec 6, 2024 • 7 min read
markus-spiske-109588-unsplash

Top 5 design patterns in OOP with TypeScript's implementation - code included.


logo-typescript

We all use design patterns in our code. Sometimes, it's unnecessary, but it could give a nice and understandable structure to your architecture. Since TypeScript is getting more popular, I decided to show some of the popular patterns with its implementation.

Singleton

{}

Overview

Singleton is most of the known patterns in the programming world. Basically, you use this pattern if you need to instantiate a restricted number of instances. You do this by making a private constructor and providing a static method, which returns an instance of the class.

Usage

This pattern is used in other patterns. Abstract factory and builder uses it in itself implementations. Sometimes, you use singletons with facades, because you want to provide only one instance of a Facade.

Code

class Singleton {
  private static instance: Singleton | null;
  private constructor() {}

  static getInstance() {
    if (!this.instance) {
      this.instance = new Singleton();
    }
    return this.instance;
  }
}

console.log(Singleton.getInstance() === Singleton.getInstance());
//true

Fluent Interface

{} (1)

Overview

Often used in testing libraries (e.g., Mocha, Cypress), a fluent interface makes code readable as written prose. It is implemented by using method chaining. Basically, every method returns this. (self) and the chaining ends when a chain method returns void. Also, other techniques used for a fluent interface - nested functions and object scoping.

Usage

As we’ve mentioned earlier, it is used to create more readable code. Also, you can easily compose objects with this pattern and create readable queries.

Code


class Book {
  private title: string | undefined;
  private author: string | undefined;
  private rating: number | undefined;
  private content: string | undefined;

  setTitle(title: string) {
    this.title = title;
    return this;
  }
  setAuthor(author: string) {
    this.author = author;
    return this;
  }
  setRating(rating: number) {
    this.rating = rating;
    return this;
  }
  setContent(content: string) {
    this.content = content;
    return this;
  }
  getInfo() {
    return `A ${this.title} book is written by ${this.author} with ${
      this.rating
    } out of 5 stars`;
  }
}

console.log(
  new Book()
    .setTitle('Voyna i Mir')
    .setAuthor('Lev Tolstoy')
    .setRating(3)
    .setContent('A very long and boring book... Once ago...')
    .getInfo(),
);

Observer

{ds}d

Overview

This pattern suggests, that you have a subject and some observers. Every time you update your subject state, observers get notified about it. This pattern is very handy when you need to tie several objects to each other with abstraction and freedom of implementation. Also, this pattern is a key part of the familiar model-view-controller (MVC) architectural pattern. Strongly used in almost every GUI library.

Usage

You have a basic Subject class, which has 3 methods: attach, detach, notify and a list of observers, which had implemented the Observer interface. Observer - it’s an interface, which has only one method - update(). You add observers by attach(), remove them by detach(), and by notify() - calling method update() in each of them.

Code

async function sleep(msec: any) {
  return new Promise(resolve => setTimeout(resolve, msec));
}

interface Observer {
  update: Function;
}

class Observable {
  constructor(protected observers: Observer[] = []) {}

  attach(observer: Observer) {
    this.observers.push(observer);
  }
  detach(observer: Observer) {
    this.observers.splice(this.observers.indexOf(observer), 1);
  }
  notify(object = {}) {
    this.observers.forEach(observer => {
      observer.update(object);
    });
  }
}

class GPSDevice extends Observable {
  constructor(private coordinates: any = { x: 0, y: 0, z: 0 }) {
    super();
  }

  process() {
    this.coordinates.x = Math.random() * 100;
    this.coordinates.y = Math.random() * 100;
    this.coordinates.z = Math.random() * 100;

    this.notify(this.coordinates);
  }
}

class Logger implements Observer {
  update(object: any) {
    console.log(`Got the next data ${object.x} ${object.y} ${object.z}`);
  }
}

class TwoDimensionalLogger implements Observer {
  update(object: any) {
    console.log(`Got the next 2D data ${object.x} ${object.y}`);
  }
}

const gps = new GPSDevice();
const logger = new Logger();
const twoDLogger = new TwoDimensionalLogger();

gps.attach(logger);
gps.attach(twoDLogger);

(async () => {
  for (let tick = 0; tick < 100; tick++) {
    await (async () => {
      await sleep(1000);
      if (tick === 3) gps.detach(logger)
      gps.process();
    })();
  }
})();

Composite

{ds}d (1)

Overview

The composite pattern is a pattern, which abstracts a group of objects into the one single instance of the same type. The composite pattern is easy to run, test and understand. It is used whenever is a need of representing a part-whole hierarchy as a tree structure and to treat part and whole object equally.

Usage

Basically, there are two ways of using this pattern: uniform and safe.

In a uniform way, you define the child-related operations in the Component interface - that means, that Leaf and Composite are treated same. The bad thing is that you lose type safety because child-related operations can be performed on Leaf objects.

In a safe way, child-related operations are defined only in the Composite class. Leaf and Composite objects are treated differently. Since child-related operations can’t be performed on Leaf, you gain type safety.

Code


interface Component {
  operation: Function;
}

abstract class Leaf implements Component {
  operation() {}
}

abstract class Composite implements Component {
  protected childs: Component[] = [];
  operation() {
    this.childs.forEach(child => {
      child.operation();
    });
  }
  add(component: Component) {
    this.childs.push(component);
  }
  remove(component: Component) {
    this.childs.splice(this.childs.indexOf(component), 1);
  }
  getChild() {
    return this.childs;
  }
}

class Duck extends Composite {
  constructor(childs: Component[]) {
    super();
    this.childs = childs;
  }
}

class DuckVoice extends Leaf {
  operation() {
    console.log('Quack.');
  }
}

class DuckFly extends Composite {
  operation() {
    console.log('It flies.');
    super.operation();
  }
  add(component: Component) {
    super.add(component);
    return this;
  }
}

class Wing extends Leaf {
  operation() {
    console.log('Flap-flap-flap');
  }
}

const wings = [new Wing(), new Wing()];
const flyAbility = new DuckFly().add(wings[0]).add(wings[1]);
const voiceAbility = new DuckVoice();

const duck = new Duck([flyAbility, voiceAbility]);

duck.operation();

Abstract factory

{ds}d (2)

Overview

Abstract factory is a specific pattern, which used to create an abstract object with an abstract factory. That basically means, that you can put every factory that implements the Abstract Factory and it would return an instance, that implements the Abstract Object interface.

Usage

You define two interfaces: an Abstract Factory’s one and a Subject’s one. Then, you implement whatever you want and expose the interface. A client doesn’t know what is inside, he just gets an object with implement methods of an interface.

Code


interface SoundFactory {
  create: Function;
}

interface Sound {
  enable: Function;
}

class FerrariSound implements Sound {
  enable() {
    console.log('Wrooom-wrooom-wrooooom!');
  }
}

class BirdSound implements Sound {
  enable() {
    console.log('Flap-flap-flap');
  }
}

class FerrariSoundFactory implements SoundFactory {
  create() {
    return new FerrariSound();
  }
}

class BirdSoundFactory implements SoundFactory {
  create() {
    return new BirdSound();
  }
}

(() => {
  let factory: SoundFactory | null = null ;

  const type = Math.random() > 0.5 ? 'ferrari' : 'bird';

  switch (type) {
    case 'ferrari':
      factory = new FerrariSoundFactory();
      break;
    case 'bird':
      factory = new BirdSoundFactory();
      break;
  }

  if (factory) {
    const soundMaker = factory.create();
    soundMaker.enable();
  }

})();

/*
ts-node abstract-factory.ts
Flap-flap-flap

ts-node abstract-factory.ts
Wrooom-wrooom-wrooooom!

ts-node abstract-factory.ts
Flap-flap-flap

ts-node abstract-factory.ts
Wrooom-wrooom-wrooooom!
*/

Summary

We have revealed some patterns implementations in this article. Thank you for your attention.

Photo of Volodymyr Voleniuk

More posts by this author

Volodymyr Voleniuk

A node.js developer...
Efficient software development  Build faster, deliver more  Start now!

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business