VOOZH about

URL: https://blog.logrocket.com/how-typescript-design-patterns-help-you-write-better-code/

⇱ How TypeScript design patterns help you write better code - LogRocket Blog


2020-06-10
1652
#typescript
Eslam Hefnawy
20034
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

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.

👁 How TypeScript Design Patterns Help You Write Better Code

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.

🚀 Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

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.


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

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

LogRocket understands everything users do in your web and mobile apps.

👁 LogRocket Dashboard Free Trial Banner

LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.

With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.

Modernize how you understand your web and mobile apps — start monitoring for free.

👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo — with email/password login, Google OAuth, session persistence, and protected routes.

👁 Image
Chinwike Maduabuchi
Jun 9, 2026 ⋅ 13 min read

AI dev tool power rankings & comparison [June 2026]

Compare the top AI development tools and models of June 2026. View updated rankings, feature breakdowns, and find the best fit for you.

👁 Image
Chizaram Ken
Jun 8, 2026 ⋅ 11 min read

How to check username availability at scale with Bloom filters

Learn how Bloom filters reduce database lookups for username availability checks while preserving correctness at scale.

👁 Image
Rosario De Chiara
Jun 8, 2026 ⋅ 6 min read
View all posts

Would you be interested in joining LogRocket's developer community?

Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now