Decorator Pattern

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 th

Winter Be Like

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:

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.

function eatable(object) {
  object.eatable = true;
  return object;
}
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.

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.

{
  "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.

function log(constructor: any) {
  console.log(`New ${constructor.name} created!`);
}
@log
class Yogurt {
  public flavor: string;
  constructor(flavor: string) {
    this.flavor = flavor;
  }
}

The issue with the code is that ourconsole.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.

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.

new Yogurt('strawberry');

new Yogurt('cherry');

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.

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.

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.

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.

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.

function enumerable(isEnumerable: boolean) {
  return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = isEnumerable;
  };
}
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.

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.

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.

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.

Resources

Last updated