JavaScript design patterns #4. Decorators and their implementation in TypeScript

栏目: IT技术 · 发布时间: 4年前

内容简介: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 mentio

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:

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

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');
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.

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.

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 decorator s 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.

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

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

部落:一呼百应的力量

部落:一呼百应的力量

高汀 (Godin.S.) / 刘晖 / 中信出版社 / 2009-7 / 26.00元

部落指的是任何一群人,规模可大可小,他们因追随领导、志同道合而相互联系在一起。人类其实数百万年前就有部落的出现,随之还形成了宗教、种族、政治或甚至音乐。 互联网消除了地理隔离,降低了沟通成本并缩短了时间。博客和社交网站都有益于现有的部落扩张,并促进了网络部落的诞生——这些部落的人数从10个到1000万个不等,他们所关注的也许是iPhone,或一场政治运动,或阻止全球变暖的新方法。 那么......一起来看看 《部落:一呼百应的力量》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具