- 1. JavaScript design patterns #1. Singleton and the Module
- 2. JavaScript design patterns #2. Factories and their implementation in TypeScript
- 3. JavaScript design patterns #3. The Facade pattern and applying it to React Hooks
- 4. JavaScript design patterns #4. Decorators and their implementation in TypeScript
- 5. JavaScript design patterns #5. The Observer pattern with TypeScript
When we are cold, we put on a shirt. If that’s not enough, we put a coat on top. Decorators work similarly. In this article, we discuss the principles of the decorator design pattern and talk about their implementation in TypeScript. We also briefly mention the JavaScript implementation of decorators using Babel.
The basics of decorators
The Decorator Pattern is one of the well-known design patterns described by the “gang of four”. The fundamental purpose of decorators is to attach new features and behaviors to already defined classes and methods. If we consider the above to be the core of decorators, it is easy to implement our decorator:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Yogurt { constructor(flavor) { this.flavor = flavor; } } function frozen(object) { return Object.freeze(object); } const yogurt = frozen( new Yogurt('strawberry') ); |
Thanks to the frozen function, we now can get an object which properties can’t be changed. The above happens thanks to the usage of the Object.freeze function. We’ve made it easily reusable, and therefore, we can attach it to an object of any class.
Furthermore, our decorators can be stacked up on top of each other. It might sometimes be tricky, though.
1 2 3 4 |
function eatable(object) { object.eatable = true; return object; } |
1 2 3 4 5 |
const yogurt = eatable( frozen( new Yogurt('strawberry') ) ); |
The above example might not work as you expect, because when we attempt to make the yogurt to be eatable, it is frozen already. We need to revert the order of our decorators.
1 2 3 4 5 |
const yogurt = frozen( eatable( new Yogurt('strawberry') ) ); |
The code above might look a bit like using Higher-Order Components with React. You could actually treat them as hooks and they are sometimes seen as such.
We can imagine that this pattern would come in handy quite often. Unfortunately, the above syntax is not very elegant. Thankfully, the efforts have been made to standardize decorators both in JavaScript and TypeScript.
Decorators in TypeScript
First, let’s look into decorators in TypeScript, because they are a built-in feature. The only thing we need to do is to turn on the experimentalDecorators flag in our tsconfig.json file.
1 2 3 4 5 |
{ "compilerOptions": { "experimentalDecorators": true } } |
As you can see above, the decorators are still an experimental feature in TypeScript, and they might change in the future.
The Decorator is a special syntax that provides us with a way to attach additional logic to classes, methods, accessors, parameters, and properties. They are used heavily in frameworks like Angular and NestJS.
Class decorator
We can declare a class decorator by adding it just before the class declaration. Its argument is the constructor of our class.
Let’s define a decorator that logs out the name of every created instance.
1 2 3 |
function log(constructor: any) { console.log(`New ${constructor.name} created!`); } |
1 2 3 4 5 6 7 |
@log class Yogurt { public flavor: string; constructor(flavor: string) { this.flavor = flavor; } } |
The issue with the code is that our console.log is called only once when our class is initialized. Thankfully, we can replace the original constructor by returning a value from our class decorator.
1 2 3 4 5 6 |
function log(constructor: any) { return function(...args: any[]) { console.log(`New ${constructor.name} created!`); return new constructor(...args); } as typeof constructor; } |
In the above code, we log out the name of the class every time we call the constructor.
1 2 |
new Yogurt('strawberry'); new Yogurt('cherry'); |
New Yogurt created!
New Yogurt created!
Method decorators
We add a method decorator just before the declaration of a method. We can use it to track, alter, or replace a method definition.
This is a great moment to use the decorator factory approach. It involves creating a function that returns a decorator. We can find it very useful when wanting to pass some additional properties to the decorator.
The method decorator has three arguments:
- the constructor function of the class (for a static property), or the prototype of the class (for instance property),
- name of the property,
- the property descriptor.
Let’s create a method decorator that removes provided properties from the return value. We might find it useful, for example, to strip out a hash of the password of a user, when returning his data.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function excludeProperties(propertiesToExclude: string[]) { return (target: any, propertyName: string, descriptor: PropertyDescriptor) => { const originalFunction = descriptor.value; descriptor.value = async function(...args: any[]) { const originalResult = await originalFunction.apply(this, args); propertiesToExclude.forEach(propertyName => { delete originalResult[propertyName]; }); return originalResult; }; } } |
In the code above, we save the reference to the original function using the descriptor.value property. We use the async/await syntax because we want to use the excludeProperties function to decorate a method that returns a promise.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import userModel from './user.model'; import UserNotFoundException from './UserNotFoundException'; import excludeProperties from './excludeProperties'; class UserService { private user = userModel; @excludeProperties(['password']) private getUser = async (userId: string) => { const user = await this.user.findById(userId); if (user) { return user; } throw new UserNotFoundException(userId); } } |
Thanks to our decorator, the getUserById function returns a user but removes the password from the return value.
If you want to know how to create an API with Node.js and Express, check out the TypeScript Express series
Property decorators
We can add property decorators just before declaring a property. It has two arguments:
- the constructor function of the class (for a static property), or the prototype of the class (for instance property),
- name of the property.
1 2 3 4 5 6 7 8 9 10 11 |
function log(target:any, propertyName: string) { console.log(`The property ${propertyName} added to the class`); } class Yogurt { @log public flavor: string; constructor(flavor: string) { this.flavor = flavor; } } |
As you can see above, the arguments do not include property descriptors. We can’t really do much with them alone, but you might find them useful when exploring the reflect-metadata library.
Accessor decorators
The accessor decorators are similar to the method decorators. The catch to them is that we can’t decorate both get and set accessors of a single property. We need to apply a decorator to the first accessor in our class.
1 2 3 4 5 |
function enumerable(isEnumerable: boolean) { return function (target: any, propertyName: string, descriptor: PropertyDescriptor) { descriptor.enumerable = isEnumerable; }; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class TranslationsService { private currentLanguage: string; previousLanguages: string [] = []; @enumerable(false) get language() { return this.currentLanguage; } set language(newLanguage: string) { if (this.currentLanguage) { this.previousLanguages.push(this.currentLanguage) } this.currentLanguage = newLanguage; } } |
Parameter decorators
We can add a parameter decorator before the declaration of a parameter inside of a function. It has three arguments:
- the constructor function of the class (for a static property), or the prototype of the class (for instance property),
- name of the property,
- the index of the parameter
Or at least the documentation states the above is the case. There seems to be some confusion regarding the second argument.
1 2 3 4 5 6 7 8 9 10 11 12 |
function log(target: any, parameterName: string, parameterIndex: number) { console.log(`Added a parameter number ${parameterIndex}`); } class Yogurt { public flavor: string; constructor( @log flavor: string ) { this.flavor = flavor; } } |
Similarly to the property decorators, the parameter decorators can just be used to observe that a parameter was declared and do some side-effects. You might find it useful with the reflect-metadata library.
Decorators without TypeScript
To use decorators, we don’t have to go with TypeScript. The proposal for decorators is currently in stage 2, which means that they are likely to become a part of the language but might change a bit.
To use decorators with JavaScript, we need Babel with the @babel/plugin-proposal-decorators plugin.
If you would like to know how to set up webpack with Babel, check out Webpack 4 course – part two. Using loaders to handle scss, image files and transpile JS
The important thing is that the syntax differs a bit. It was changed when the proposal reached the second stage. For a more detailed comparison, check out this article.
If you would like to use the Babel decorators with the older syntax, you can use the legacy flag.
.babelrc
1 2 3 4 5 |
{ "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }] ] } |
Summary
The decorator pattern proves to be useful when we want to assign some additional behavior. In this article, we’ve gone through all of the types of decorators that we can use. We’ve also defined some use-cases for them. When discussing decorators, we’ve focused on TypeScript, but we’ve also briefly touched the subject of decorators in JavaScript with Babel. They are an elegant and reusable solution and allow us to delegate some of the logic from outside of our classes, making the Single Responsibility Principle easier to keep.