Navigate back to the homepage
Browse Repo

3 Design Patterns in TypeScript for Frontend Developers

Samaila Bala
June 22nd, 2021 · 5 min read

Design Patterns are best software practices used by Software Developers in solving recurring problems in Software Development. They aren’t code-related but rather a blueprint to use in designing a solution for a myriad of use cases.

There are about 23 different Software Design Patterns put together by the Gang of Four that can be grouped into three different categories:

  • Creational Pattern: These are patterns that pertain to object creation and class instantiation they help in the reuse of an existing code. The major creational patterns are Factory Method, Abstract Factory, Builder, Prototype, and Singleton.
  • Structural Pattern: These are patterns that help simplify the design by identifying a way to create relationships among entities such as objects and classes. They are concerned with how classes and objects can be assembled into larger structures. Some of the design patterns that fall into this category are: Adapter, Decorator, and Proxy.
  • Behavioral Pattern: These are patterns that are concerned with responsibilities among objects in order to help increase flexibility in carrying out communication. Some of these patterns are Observer, Memento, and Iterator.

In this article, we will look at three different patterns and how to use each of these patterns with TypeScript. This article assumes the reader knows JavaScript and TypeScript to follow along although these concepts can also be applied to other Object-oriented programming languages.


The Singleton pattern is one of the most common design patterns. According to Refactoring Guru:

Singleton is a creational design pattern that lets you ensure that a class has only one instance, while providing a global access point to this instance.

In a Singleton pattern, a class or object can only be instantiated once, and any repeated calls to the object or class will return the same instance. This single instance is what is refered to as a Singleton.

A good example of a Singleton in Frontend applications is the global store in popular state management libraries like Vuex and Redux. The global store is a singleton as it is accessible in various parts of the application and we can only have one instance of it.

A singleton can also be used to implement a Logger to manage logs across an application. The logger is a great choice as a singleton because we will always want all our logs in one place in case we need to track them. Let’s see how we can implement a Logger with a Singleton in TypeScript

1class Logger {
2 private static instance: Logger;
3 private logStore:any = []
4 private constructor() { }
5 public static getInstance(): Logger {
6 if (!Logger.instance) {
7 Logger.instance = new Logger();
8 }
9 return Logger.instance;
10 }
11 public log(item: any): void{
12 this.logStore.push(item)
13 }
14 public getLog():void{
15 console.log(this.logStore)
16 }

In the example above we’ve created a logger to log items across an application. The constructor is made private to prevent creating a new instance of the class with the new keyword. The getInstance method will only create a new instance if there isn’t an existing instance thereby obeying the singleton principle.

Let’s see how we can use the singleton created

1const useLogger = Logger.getInstance()
2useLogger.log('Log 1')
3const anotherLogger = Logger.getInstance()
4anotherLogger.log('Log 2')

If you run the program above you’ll notice anotherLogger didn’t create another instance but rather used the existing instance.

As common as singletons are they tend to be considered as an anti-pattern in some circles because of how overused they are, and the fact that they introduce a global state into an application, so before you use a singleton please consider if that will be the best use case for what you are trying to implement.


The Observer pattern is pretty common in TypeScript. It specifies a one-to-many relationship between an object and its dependents such that when the object changes state it notifies the other objects that depend on it about the change in state. The observer pattern is also common in the major frontend frameworks and libraries as the whole concept of updating parts of a UI with response to events comes from it.

In TypeScript, the observer pattern provides a way for UI components to subscribe to changes in an object without direct coupling to classes. A perfect example of the Observer pattern is a Mailing list. If as a user you are subscribed to a mailing list it sends you messages so you don’t have to manually check for a new message from the subject. Let’s look at how to implement this in TypeScript

1interface NotificationObserver {
2 onMessage(message: Message): string;
5interface Notify {
6 sendMessage(message: Message): any;
9class Message {
10 message: string;
12 constructor(message: string) {
13 this.message = message;
14 }
16 getMessage(): string {
17 return `${this.message} from publication`;
18 }
21class User implements NotificationObserver {
22 element: Element;
24 constructor(element: Element) {
25 this.element = element;
26 }
28 onMessage(message: Message) {
29 return (this.element.innerHTML += `<li>you have a new message - ${message.getMessage()}</li>`);
30 }
33class MailingList implements Notify {
34 protected observers: User[] = [];
36 notify(message: Message) {
37 this.observers.forEach((observer) => {
38 observer.onMessage(message);
39 });
40 }
42 subscribe(observer: User) {
43 this.observers.push(observer);
44 }
45 unsubscribe(observer: User) {
46 this.observers = this.observers.filter(
47 (subscriber) => subscriber !== observer
48 );
49 }
51 sendMessage(message: Message) {
52 this.notify(message);
53 }
56const messageInput: Element = document.querySelector(".message-input");
58const user1: Element = document.querySelector(".user1-messages");
59const user2: Element = document.querySelector(".user2-messages");
61const u1 = new User(user1);
62const u2 = new User(user2);
64const subscribeU1: Element = document.querySelector(".user1-subscribe");
65const subscribeU2: Element = document.querySelector(".user2-subscribe");
67const unSubscribeU1: Element = document.querySelector(".user1-unsubscribe");
68const unSubscribeU2: Element = document.querySelector(".user2-unsubscribe");
70const sendBtn: Element = document.querySelector(".send-btn");
72const mailingList = new MailingList();
77subscribeU1.addEventListener("click", () => {
78 mailingList.subscribe(u1);
80subscribeU2.addEventListener("click", () => {
81 mailingList.subscribe(u2);
84unSubscribeU1.addEventListener("click", () => {
85 mailingList.unsubscribe(u1);
87unSubscribeU2.addEventListener("click", () => {
88 mailingList.unsubscribe(u2);
91sendBtn.addEventListener("click", () => {
92 mailingList.sendMessage(new Message(messageInput.value));

In the example above the Notify interface contains a method for sending out messages to subscribers. The NotificationObserver checks to see if there are any messages and alert the subscribed users. The Message class holds the message state and it notifies subscribed users whenever the message state changes thereby following the observer pattern. So users can choose to subscribe or unsubscribe to messages. A complete working example of the code is available here.

Open Source Session Replay

Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder. OpenReplay is the only open-source alternative currently available.


Happy debugging, for modern frontend teams - Start monitoring your web app for free.

Factory Method

The factory method is a creational pattern that deals with Object creation. It helps in encapsulating an object from the code that depends on it. This might be confusing so let me use an analogy to explain. Imagine having a vehicle plant that produces different vehicles and you start by producing sedans but later on you decide to go into the production of trucks, you’ll probably have to create a duplicate production system for the trucks, now let’s imagine you add SUVs and minivans to the mix. At this point, the production system becomes messy and repetitive.

In a factory pattern, we can abstract the common behavior among the vehicles like how the vehicle is made, into a separate interface object called Vehicle, and then allow the different implementations to implement this common behavior in their unique ways.

In frontend, a factory method pattern allows us to abstract common behavior among components, let’s imagine a Toast component that has a different behavior on Mobile and Desktop we can use TypeScript to create a toast interface that outlines the general behavior of the toast component

1interface Toast {
2 template: string;
3 title: string;
4 body: string;
5 position: string;
6 visible: boolean;
7 hide(): void;
8 render(title: string, body: string, duration: number, position: string): string

After creating a common interface that contains the general behavior of the toast component, the next step is creating the different implementations (Mobile and Desktop) of the toast interface

1class MobileToast implements Toast {
2 title: string;
3 body: string;
4 duration: number;
5 visible = false;
6 position = "center"
7 template = `
8 <div class="mobile-toast">
9 <div class="mobile-toast--header">
10 <h2>${this.title}</h2>
11 <span>${this.duration}</span>
12 </div>
13 <hr/>
14 <p class="mobile-toast--body">
15 ${this.message}
16 </p>
17 </div>
18 `;
19 hide() {
20 this.visible = false;
21 }
22 render(title: string, body: string, duration: number, position: string) {
23 this.title = title,
24 this.body = body
25 this.visible = false
26 this.duration = duration
27 this.position = "center"
28 return this.template
29 }
32class DesktopToast implements Toast {
33 title: string;
34 body: string;
35 position: string
36 visible = false;
37 duration: number;
38 template = `
39 <div class="desktop-toast">
40 <div class="desktop-toast--header">
41 <h2>${this.title}</h2>
42 <span>${this.duration}</span>
43 </div>
44 <hr/>
45 <p class="mobile-toast--body">
46 ${this.message}
47 </p>
48 </div>
49 `;
50 hide() {
51 this.visible = false;
52 }
53 render(title: string, body: string, duration: number, position: string) {
54 this.title = title,
55 this.body = body
56 this.visible = true
57 this.duration = duration
58 this.position = position
59 return this.template
60 }

As you can see from the code, the Mobile and Desktop have slightly different implementations but maintain the base behavior of the toast interface. The next step is creating a factory class that the Client code will work with without having to worry about the different implementations we’ve created.

1class ToastFactory {
2 createToast(type: 'mobile' | 'desktop'): Toast {
3 if (type === 'mobile') {
4 return new MobileToast();
5 } else {
6 return new DesktopToast();
7 }
8 }

The factory code will return the correct implementation of the Toast component based on the type that is passed to it as a parameter. At this point, we can write our client code to communicate with the ToastFactory .

1class App {
2 toast: Toast;
3 factory = new ToastFactory();
4 render() {
5 this.toast = this.factory.createToast(isMobile() ? 'mobile' : 'desktop');
6 if (this.toast.visible) {
7 this.toast.render('Toast Header', 'Toast body');
8 }
9 }

You can see the app isn’t worried about our earlier implementations. Yeah I know it is a lot of code but this process ensures that the component implementations are not directly coupled to the component itself. This makes it easier, in the long run, to extend the component without breaking the existing code.


We’ve looked at some design patterns and their implementations in TypeScript. As earlier mentioned these design patterns provide a blueprint to follow when faced with recurring problems in Software Development. The examples in the article should be used as a guide to get started. I can’t wait to see how you apply them to the applications you create.

More articles from OpenReplay Blog

React 18 Is Out! This Is What You Need to Know

React 18 has been announced, but what's new about it? Here are the details you need to know about this new version of React.

June 18th, 2021 · 7 min read

Redux Alternatives in 2021

Should you be using Redux in 2021? Isn't there a better alternative for your use case?

June 11th, 2021 · 5 min read
© 2021 OpenReplay Blog
Link to $ to $ to $