preface

How to do authentication gracefully? How to elegantly print function entries and results? How to gracefully run time data checking? Decorators are never late. Try a decorator!

What is a Decorator?

A Decorator, one of the proposals in ES6, is actually a wrapper that provides additional functionality to a class, property, or function. An 🌰 :

function f(key: string) :any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

@f("Class Decorator")
class A {
  constructor(@f("Constructor Parameter") foo) {}

  @f("Instance Method") / / 1
  method(@f("Instance Method Parameter") foo) {} / / 2

  @f("Instance Property") prop? :number;
}

// Basically, the decorator will behave like this:

@f(a)class A/ / is equivalent toA = f(A) || A
Copy the code

Preparation before use

Although the Decorator is just a proposal, it can be used with tools:

Babel:

babel-plugin-syntax-decorators babel-plugin-transform-decorators-legacy

Typescript:

The command line:

tsc --target ES5 --experimentalDecorators
Copy the code

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES5"."experimentalDecorators": true}}Copy the code

Execution order

The execution order of the different types of decorators is clear: 1. Instance member: Parameter decorator -> method/accessor/property decorator 2. Static member: Parameter decorator -> method/accessor/property decorator 3.

function f(key: string) :any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

@f("Class Decorator")
class A {
  @f("Static Property")
  staticprop? :number;

  @f("Static Method")
  static method(@f("Static Method Parameter") foo) {}

  constructor(@f("Constructor Parameter") foo) {}

  @f("Instance Method")
  method(@f("Instance Method Parameter") foo) {}

  @f("Instance Property") prop? :number;
}

// Order of execution
evaluate:  Instance Method
evaluate:  Instance Method Parameter
call:  Instance Method Parameter
call:  Instance Method
evaluate:  Instance Property
call:  Instance Property
evaluate:  Static Property
call:  Static Property
evaluate:  Static Method
evaluate:  Static Method Parameter
call:  Static Method Parameter
call:  Static Method
evaluate:  Class Decorator
evaluate:  Constructor Parameter
call:  Constructor Parameter
call:  Class Decorator
Copy the code

However, different argument constructors in the same method are in reverse order, and the last argument back decorator is executed first:


function f(key: string) :any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

class B {
  @f('first')
  @f('second')
  method(){}}// Order of execution
evaluate:  first
evaluate:  second
call:  second
call:  first
Copy the code

define

Class decorator

📌 parameters:

  • target: a class ofConstructor

⬅ ️ return values: undefined | replacing the constructor

Therefore, class decorators are suitable for inheriting an existing class and adding properties and methods.

function rewirteClassConstructor<T extends { new(... args:any[]) : {}} > (constructor: T) {
  return class extends constructor {
    words = "rewrite constructor";
  };
}
 
@rewirteClassConstructor
class Speak {
  words: string;
 
  constructor(t: string) {
    this.words = t; }}const say = new Speak("hello world");
console.log(say.words) // rewrite constructor
Copy the code

Attribute decorator

📌 parameters:

  • target: is the constructor of the class for static members and the prototype chain of the class for instance members
  • propertyKey: Attribute name

⬅️ Return value: The returned result will be ignored

In addition to being used to gather information, attribute decorators can also be used to add additional methods and attributes to a class. For example, we can write a decorator to add listeners to some properties.

import "reflect-metadata";

function capitalizeFirstLetter(str: string) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

function observable(target: any, key: string) :any {
  // prop -> onPropChange
  const targetKey = "on" + capitalizeFirstLetter(key) + "Change";

  target[targetKey] = function (fn: (prev: any, next: any) = >void) {
      let prev = this[key];
      // tsconfig.json target to ES6
      Reflect.defineProperty(this, key, {
        set(next){ fn(prev, next); prev = next; }})}; }class C {
  
  @observable
  foo = -1;

  onFooChange(arg0: (prev: any, next: any) = >void){}}const c = new C();

c.onFooChange((prev, next) = > console.log(`prev: ${prev}, next: ${next}`))

c.foo = 100; // -> prev: -1, next: 100
c.foo = -3.14; // -> prev: 100, next: -3.14
Copy the code

Method decorator

📌 parameters:

  • target: is the constructor of the class for static members and the prototype chain of the class for instance members
  • propertyKey: Attribute name
  • descriptorAttributes of the:The descriptor

⬅ ️ return values: undefined | replace property descriptor.

The key of the method descriptor is:

value
writable
enumerable
configurable
Copy the code

With this parameter we can modify the method’s original implementation to add some common logic. For example, we can add the ability to print inputs and outputs to some methods:

function logger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;

  descriptor.value = function (. args) {
    console.log('params: '. args);const result = original.call(this. args);console.log('result: ', result);
    returnresult; }}class C {
  @logger
  add(x: number, y:number ) {
    returnx + y; }}const c = new C();
c.add(1.2);
// -> params: 1, 2
// -> result: 3
Copy the code

Accessor decorator

📌 parameters:

  • target: is the constructor of the class for static members and the prototype chain of the class for instance members
  • propertyKey: Attribute name
  • descriptorAttributes of the:The descriptor

⬅ ️ return values: undefined | replace property descriptor.

The accessor descriptor key is:

get
set
enumerable
configurable
Copy the code

Accessor decorators are generally similar to method decorators. The only difference is that the descriptor has different keys. For example, we can set a property to an immutable value:

function immutable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.set;

  descriptor.set = function (value: any) {
    return original.call(this, { ...value })
  }
}

class C {
  private _point = { x: 0.y: 0 }

  @immutable
  set point(value: { x: number, y: number }) {
    this._point = value;
  }

  get point() {
    return this._point; }}const c = new C();
const point = { x: 1.y: 1 }
c.point = point;

console.log(c.point === point)
// -> false
Copy the code

Parameter decorator

📌 parameters:

  • target: is the constructor of the class for static members and the prototype chain of the class for instance members
  • propertyKey: attribute name (method name, not parameter name)
  • paramerterIndex: The subscript of the argument’s position in a method

⬅️ Return value: The returned value will be ignored.

A parameter decorator alone can do very little; it is generally used to record information that can be used by other decorators.

// parameter.ts
import "reflect-metadata";

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata('required', target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata('required', existingRequiredParameters, target, propertyKey);
}

function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    letmethod = descriptor.value! ; descriptor.value =function () {
      let requiredParameters: number[] = Reflect.getOwnMetadata('required', target, propertyName);
      if (requiredParameters) {
        for (let parameterIndex of requiredParameters) {
          if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
            throw new Error("Missing required argument."); }}}return method.apply(this.arguments);
    };
  }

class BugReport {
    type = "report";
    title: string;
   
    constructor(t: string) {
      this.title = t;
    }
   
    @validate
    print(@required verbose: boolean) {
      if (verbose) {
        return `type: The ${this.type}\ntitle: The ${this.title}`;
      } else {
       return this.title; }}}export const report = new BugReport('mode error');
Copy the code
// test.js

const { report } = require('./paramerter.js');
console.log(report.print()); // Error: Missing required argument.
Copy the code

Usage scenarios

  • Before/After the hook.
  • Listen for property changes or method calls.
  • Convert method parameters.
  • Add additional methods and attributes.
  • Runtime type checking.
  • Automatic encoding and decoding.
  • Dependency injection.

Using an example

  • Log print
function f() :any {
  return function (target, key, descriptor) {
    let method = descriptor.value;
    descriptor.value = function () {
      console.log('param: '.Array.from(arguments));
      const value = method.apply(this.arguments);
      console.log('result: ', value);
      return value
    };
  };
}

class B {
  @f(a)say(name: string) {
      return `name is ${name}`; }}Copy the code
  • Authentication:
function auth(user) {
  return function(target, key, descriptor) {
    var originalMethod = descriptor.value; // Keep the original function
    if(! user.isAuth) { descriptor.value =function() { // A prompt will be displayed if you do not log in
        console.log('Not currently logged in, please log in! '); }}else {
      descriptor.value = function (. args) { // The original function is logged in
        originalMethod.apply(this, args); }}returndescriptor; }}@auth(app.user)
function handleStar(new) {
  new.like++;
}
Copy the code
  • Type checking
import "reflect-metadata";
const stringMetaDataTag = "IsString";
 
function IsString(target: Object, propertyKey: string | symbol, parameterIndex: number) {
  let existingRequiredParameters: number[] = Reflect.getOwnMetadata(stringMetaDataTag, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata( stringMetaDataTag, existingRequiredParameters, target, propertyKey);
}
 
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
  letmethod = descriptor.value! ; descriptor.value =function () {
    let stringMetaTags: number[] = Reflect.getOwnMetadata(stringMetaDataTag, target, propertyName);
    if (stringMetaTags) {
      for (let parameterIndex of stringMetaTags) {
        const value = arguments[parameterIndex];
        if(! (valueinstanceof String || typeof value === 'string')) {
            throw new Error('not string'); }}}return method.apply(this.arguments);
  };
}


export class A {
    a: string = '123';
    
    @validate
    value (@IsString value: string) {
        console.log(value);
        this.a = value; }}Copy the code

.

Write in the last

The author has practiced in background interface, Js Bridge and React projects. It has to be said that the decorator pattern is almost “best practice” for aspect oriented programming (AOP) and greatly improves programming efficiency. I also hope this article will help you 😊

NPM package

  • class-validator
  • core-decorators
  • Nest Background Framework

Refer to the link

  • tc39-proposal
  • typescript
  • a-complete-guide-to-typescript-decorator