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.
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:
After reading this tutorial, you should:
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.
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.
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 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.
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.
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 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.
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.
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.
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.
Design patterns can be classified into three categories based on their intended purpose and the type of problems they solve.
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.
Let’s take a high-level look at everything that we went over in this guide.
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.
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.
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 nowCreate a multi-lingual web application using Nuxt 3 and the Nuxt i18n and Nuxt i18n Micro modules.
Use CSS to style and manage disclosure widgets, which are the HTML `details` and `summary` elements.
React Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
3 Replies to "How TypeScript design patterns help you write better code"
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.
To be fair, JavaScript came before and led to Typescript therefore Javascript can be thought of as its predecessor.
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.