Eslam Hefnawy Serverless Architect at Serverless, Inc. Co-creator of the Serverless Framework and the lead architect of Serverless Components.

How TypeScript design patterns help you write better code

5 min read 1652

How TypeScript Design Patterns Help You Write Better Code

TypeScript is a language that has seen a lot of exposure in the modern world of software engineering. Its powerful, strict type system reduces the likelihood of running into errors during runtime or hidden bugs in production due to the lack of strong types in, JavaScript, TypeScript’s predecessor.

While TypeScript makes the development experience smooth on its own, it’s important to know how a design pattern can help retain or improve the efficiency and usefulness of a codebase. Using the right design pattern in the right situation and knowing how to do so is a crucial skill in any developer’s toolbox.

Prerequisites and outcomes

The implementation language is TypeScript, but design patterns are language-agnostic, so the concept and the logic behind them are what is most important. To follow along, you should have:

  • A strong background in JavaScript and TypeScript
  • A good understanding of data types

After reading this tutorial, you should:

  • Understand the importance of design patterns in a modern codebase
  • Know how to implement the observer, builder, and prototype design patterns in TypeScript
  • Understand the concept behind each design pattern and be capable of implementing them in any language

The observer pattern: I know what happened to you

The observer design pattern creates a system to which objects subscribe. When there’s a change to an object they’re observing, all subscribers are immediately notified. This circumvents the necessity to notify all objects when a change happens in a specified object.

Concept

The observer design pattern involves two groups: the ones that subscribe to changes (observers) and the one(s) whose changes are observed (object). The object must have an array or list to hold the observers. This list or array is manipulated by two methods to either add or remove an observer. There must also be a method to notify all the observers when the object changes.

Implementation

Think of a situation where multiple patients did their tests at a hospital for the nCovid-19 virus. They are now awaiting their test results. To notify them when their results are ready, you can use the observer design pattern.

An interface of the observer is created to define its structure and types. Each observer has a method called sendMessage() that is called during the notification process. It does not return anything.

interface IObserver {
    sendMessage(): void
}

The Hospital is the object. It contains an array of observers, two methods to manipulate the array, and a method to notify all the observers.

class Hospital {
    private observers: IObserver[];

    constructor() {
        this.observers = [];
    }

    addObserver(observer: IObserver) {
        this.observers.push(observer);
        console.log('Observer added!');
    }

    removeObserver(observer: IObserver) {
        const obsIndex = this.observers.indexOf(observer);
        if (obsIndex < 0) {
            console.error('Observer does not exist!');
            return;
        }
        this.observers = this.observers.filter(obs => obs !== observer);
        console.log('Observer has been removed!');
    }

    notifyPatients() {
        console.log('Sending test result status to all patients:');
        this.observers.forEach(observer => observer.sendMessage());
    }
}

The patients are the observers. As such, they’ll have to follow the structure defined by the interface for an observer.

class Patient implements IObserver {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    sendMessage(): void {
        console.log(`${this.name}, your test results are now ready!`);
    }
}

Let’s say patients John and Jack are awaiting their test results and have subscribed to the hospital, as soon as the test results are available, they will all be alerted.

const hospital = new Hospital()
const john = new Patient('John')
const jack = new Patient('Jack')

hospital.addObserver(john)
hospital.addObserver(jack)

hospital.notifyPatients()

You can use the observer design pattern when there is a need to change the state of a bunch of objects when the state of a single object has changed where the sum of objects is not known. Most subscription systems use the observer pattern at some point. Event listeners in JavaScript are also observers.

The builder pattern: Few subclasses, few problems

The builder design pattern is self-explanatory. It creates objects but it really shines is when there is a need to create multiple objects with some similarities. Essentially, it allows you to write all the logic to create multiple different objects in the same place.



Concept

A builder avoids the necessity to create myriad subclasses from a base class or big constructors with a lot of conditional logic using method chaining and by having a single method to add each property to the object. There is one final method to return the object that has been built. Optionally, you could include a method to reset the builder.

Implementation

Think of a test result in a medical setting. A test result object can have different properties and a few common ones since there are different types of tests. The results of a pregnancy test have different properties than the results of an anemia test. There are also times when a doctor might test for pregnancy and anemia at the same time. This creates an opportunity to use the builder pattern.

As usual, we must first create an interface for each test result object to define its structure. HCG is a hormone that is tested for during a pregnancy test. RBC stands for red blood cells, and the availability of RBCs is tested during anemia tests.

interface ITestResult {
  name: string;
  rbcCount: string;
  hcgLevel: string;
}

Each property is defined using its method. Each method must return the builder to allow method chaining. Once, the object is ready, it can be built/returned.

class TestResultBuilder {
  private readonly testResult: ITestResult;

  constructor() {
    this.testResult = {
      name: '',
      rbcCount: '',
      hcgLevel: '',
    };
  }

  name(name: string): TestResultBuilder {
    this.testResult.name = name;
    return this;
  }

  rbcCount(rbcCount: string): TestResultBuilder {
    this.testResult.rbcCount = rbcCount;
    return this;
  }

  hcgLevel(hcgLevel: string): TestResultBuilder {
    this.testResult.hcgLevel = hcgLevel;
    return this;
  }

  build(): ITestResult {
    return this.testResult;
  }
}

You can use the builder pattern to avoid a common issue known as telescoping constructors, which is when you create smaller versions of a big constructor to reduce the amount of logic in the constructor. By using the builder pattern instead of the telescoping constructors’ pattern, you can save yourself the hassle of maintaining multiple constructors.

The proxy pattern: The ideal middleman

The proxy pattern is an underrated yet powerful design pattern that helps solve some very difficult problems. As the name suggests, the proxy pattern’s goal is to create a substitute for something else so that everything has to go through the substitute first to reach the actual object.

Concept

The concept behind the proxy design pattern is fairly simple: you have to create a layer above the actual object that everything else has to interact with to get to the actual object. To do this, you must have a reference to the actual object in the proxy object.

Implementation

Think of a logger that prints some kind of message to the console. You want to include the date and time when each message is printed, but only if the feature is enabled. This logic can be included in the proxy.


More great articles from LogRocket:


An interface is created for the logger with a print() method that returns nothing. This interface declares the structure of both the proxy and the actual object.

export interface Logger {
    print(): void;
}

The proxy has a method called checkEnabled(), which has some logic to determine whether the feature has been enabled. The proxy will have an identical print method that will add the date and time to the message if the feature has been enabled.

class Proxy implements Logger {
    private actualLogger: ActualLogger;

    private checkEnabled(): boolean {
        // Some logic to check if 
        // the feature is enabled
        return true;
    }

    public print(message: string): void {
        if(this.checkEnabled()) {
            const actualMessage = `
            [${new Date().toLocaleString()}]: ${message}`;
            this.actualLogger.print(actualMessage);
        }
    }
}

The actual logger will have the same print() method without the excess logic to discern whether the feature is enabled or disabled.

class ActualLogger implements Logger {
    public print(message: string): void {
        console.log(message);
    }
}

Proxies are very powerful when used right. You can use a proxy can to perform lazy loading (virtual proxy), implement caching (cache proxy), and control access to objects (security proxy). There are various types of proxies that perform different tasks, but the underlying concept remains consistent.

Use the right design pattern in the right situation

Design patterns can be classified into three categories based on their intended purpose and the type of problems they solve.

  • Structure-based design patterns describe the logic of the process of putting objects together while ensuring reusability and efficiency (e.g., the proxy pattern)
  • Behavior-based design patterns describe relationships and responsibilities between objects (e.g., the observer pattern)
  • Creation-based design patterns describe the process of making objects while ensuring that code can be reused (e.g., the builder pattern)

Knowing how to categorize design patterns directly lends itself to identifying when or when not to use a design pattern. Combining this knowledge with the design pattern itself will empower you to improve the quality of your code.

Summary

Let’s take a high-level look at everything that we went over in this guide.

  • There are three types of design patterns, classified based on their purpose: structure-based, behaviour-based and creation-based
  • The observer pattern is a behavior-based pattern that allows several objects to be notified of a change in state associated with a particular object
  • The builder pattern is a creation-based pattern that allows you to create a lot of different objects without having to overload constructors or create many subclasses
  • The proxy pattern is a structure-based pattern that allows for a substitute of an object and that everything has to go through to reach the actual object

Most importantly, design patterns are language-agnostic and can be implemented in any language to solve the kind of problem that a particular design pattern intends to solve.

Check out the following resources to learn more.

  • TypeScript documentation contains resources for JavaScript developers who want to learn TypeScript, as well as a beginner-friendly introduction to TypeScript
  • JavaScript Design Patterns,” a book by Addy Osmani, explores the design patterns implemented in JavaScript with compelling visuals

 

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Eslam Hefnawy Serverless Architect at Serverless, Inc. Co-creator of the Serverless Framework and the lead architect of Serverless Components.

3 Replies to “How TypeScript design patterns help you write better code”

  1. Why would you write your own observer pattern when you can use a framework like rxjs? Also,JavaScript is not the ‘predecessor’ of Typescript, TypeScript is the superset and is compiled into JavaScript. Finally, in the ‘what you’ll learn section’ you talk about the prototype pattern but I think you mean the proxy pattern.
    Using a visual language like UML can really help I driving your point and makes it more tangible.

  2. To be fair, JavaScript came before and led to Typescript therefore Javascript can be thought of as its predecessor.

  3. Why would you pull external library if the only you need is a patter that you can implement yourself in few lines of code? This article is fantastic explanation of Observer pattern. By the way, rxjs serves a much broader purpose than only Observer pattern.

Leave a Reply