14. Decorators

Keywords: Javascript Attribute less JSON

introduce

Additional features are needed in some scenarios to support annotation or modification of classes and members, providing a way for us to add annotations to class declarations and members through metaprogramming grammar

Note: Decorators are an experimental feature that may change in future versions.

Start: Enable the experimentalDecorators compilation option in tsconfig.json

Definition:

  • Decorator is a special type of declaration that can be attached to class declarations, methods, accessors, attributes, or parameters.
  • Decorator uses the @expression form, which must be a function after evaluation. It will be called at run time, and the Decorator's declaration information is passed in as a parameter.
function decoratFn() {
  console.log("decoratFn(): evaluated");
  return function (target, propertyKey: string, descriptor: PropertyDecorator){
    console.log('decoratFn(): called');
  }
}
function decoratG() {
  console.log('decoratG(): evaluated');
  return function (target, propertyKey: string, descriptor: PropertyDecorator){
    console.log('decoratG(): called');
  }
}
class DecoratC {
  @decoratFn()
  @decoratG()
  method() {}
}

let desc = new DecoratC();

Decorator Factory: To customize how a decorator is applied to a declaration, you need a decorator factory: a simple function that returns an expression for the decorator to call at runtime.
Decorator Combination: Multiple Decorators can be applied to a declaration at the same time

Note: When multiple decorators are applied to a declaration, they are evaluated in a way similar to the composite function. For example, when compound F and G are applied, the composite result (f.g)(x) is equivalent to f(g(x)).

In TS, when multiple decorators are applied to a declaration, the following steps are performed:

  • Evaluating Decorator Expressions from Top to Bottom
  • The result of the evaluation is called as a function from top to bottom.

Decorators on different declarations in the class are applied in the specified order:

  • Parametric Decorators, followed by Method Decorators, Accessor Decorators, or Attribute Decorators, are applied to each instance member.
  • Parametric Decorators, followed by Method Decorators, Accessor Decorators, or Attribute Decorators, are applied to each static member.
  • Parametric Decorators Applied to Constructors
  • Class Decorator Applied to Class

Class Decorator

  • Declarations: Decorators are declared before class declarations (next to class declarations)
  • Application: Decorators are applied to constructors that can be used to monitor, modify, or replace class definitions
  • Note: Class decorators cannot be used in declaration files (.d.ts) or in any external context
  • Call: Decorator class expressions are called as function calls at run time, and class constructors are their only parameters
  • Return value: If the accumulator returns a value, it replaces the class declaration with the constructor provided
  • Return Value Note: If you want to return a new constructor, you must take care of the original prototype chain. The prototype chain will not be processed in the decorator call logic at run time.
// Class Decorator, Handling Prototype Chain Problem, Class Decorative Seal Current Class
function sealed(constructor: Function){
  console.log('Decorators: sealed')
  Object.seal(constructor);
  Object.seal(constructor.prototype)
}
// When @sealed is executed, it will seal such constructors and prototypes
@sealed
class Greeter {
  greeting: string;
  constructor(message: string){
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}
let greeter = new Greeter('Bob');
greeter.greet();
// overloaded constructor
function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    newProperty = 'new property';
    hello = 'override';
  }
}
@classDecorator
class Greeters {
  property = 'property';
  hello: string;
  constructor(m: string){
    this.hello = m;
  }
}
console.log(new Greeters('world'));

Method Decorator

  • Declarations: Declarations precede the declaration of a method (next to the method declaration)
  • Application: Attribute descriptors applied to methods that can be used to monitor, modify, or replace method definitions
  • Note: Method Decorators cannot be used in declaration files (.d.ts), overloads, or any external context
  • Call: The method decorator expression is called at run time as a function
  • Return value: If the method decorator returns a value, it will be used as an attribute description of the method

Methods The following three parameters were introduced into the decorator:

  • For static members, it's a constructor of a class, and for instance members, it's a prototype object of a class.
  • Name of member
  • Attribute Description of Members
  • Note: If the code output target is less than ES5, the attribute descriptor will be undefined
// Method Decorator
function enumerable(value: boolean) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor){
    descriptor.enumerable = value;
  }
}
class GreeterM {
  greeting: string;
  constructor(message: string){
    this.greeting = message;
  }

  @enumerable(false)
  greet() {
    return 'Hello, ' + this.greeting;
  }
}

Access Decorator

  • Declarations: Access Decorator Declarations precede a Visitor Declarations (next to Visitor Declarations)
  • Application: Attribute descriptors applied to accessors and can be used to monitor, modify, or replace the definition of an accessor
  • Note: Access Decorators cannot be used in declaration files (.d.ts), or in any external context.
  • Use Note: TS does not allow a member's get and set accessors to be decorated at the same time. All decorated accessors of a member must be applied to the first accessor in the document order, because when the decorator is applied to a property descriptor, it combines get and set accessors instead of declaring them separately.
  • Call: Accessor Decorator expressions are called at runtime as functions
  • Return value: If the accessor decorator returns a value, it will be used as the property descriptor of the method

The accessor decorator passes in the following three parameters:

  • For static members, it is the constructor of a class, and for instance Chen Guan is the prototype object of a class.
  • Name of member
  • Attribute Description of Members
  • If the target version of the code output is less than ES5, the Property Descriptor will be undefined
// Accessor Decorator
function configruable(value: boolean){
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor){
    descriptor.configurable = value;
  }
}
class Point {
  private _x: number;
  private _y: number;
  constructor(x: number, y: number){
    this._x = x;
    this._y = y;
  }

  @configruable(false)
  get x() { return this._x };

  @configruable(false)
  get y() { return this._y };
}

Attribute Decorator

  • Declarations: Attribute Decorator Declarations precede an Attribute Declarations (next to the Attribute Declarations)
  • Note: Attribute Crashers cannot be used in declaration files (.d.ts), or in any external context.
  • Call: Attribute decorator is called at runtime as a function

Attribute decorator passes in two parameters:

  • For static members, it's a constructor of a class, and for instance members, it's a prototype object of a class.
  • Name of member
  • Note: Attribute descriptors are not passed into attribute decorators as parameters, which is related to how TS initializes the decorator, because there is currently no way to describe an instance attribute when defining a member of a prototype object, and there is no way to monitor or modify the initialization method of an attribute, and the return value will be ignored, so attributes Descriptors can only be used to monitor whether an attribute of a name is alive in a class
// Attribute modifier
import "reflect-metadata";
const formatMetadataKey = Symbol('format');
function format(formatString: string){
  return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
  return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
// @ Form ("Hello,% s") decorator is a decorator factory. When @format ("Hello,% s") is called, it adds a metadata for this property.
// Through the Reflect.metadata function in the reflect-metadata library. When getFormat is called, it reads the metadata of the format
class GreeterP {
  @format('Hello, %s')
  greeting: string;

  constructor(message: string){
    this.greeting = message;
  }
  greet() {
    let formatString = getFormat(this, 'greeting');
    return formatString.replace("%s", this.greeting);
  }
}

let greep = new GreeterP('world');
let greepStr = greep.greet();

Parametric Decorator

  • Declarations: The parameter decorator declares before a parameter declaration (next to the parameter declaration)
  • Application: The parameter decorator applies to class constructors or method declarations
  • Note: The parameter decorator cannot be used in declaration files (.d.ts), overloads or other external contexts
  • Call: The parameter decorator expression is called at run time as a function
  • The return value of the parameter decorator is ignored

The parameter decorator passes in the following three parameters:

  • For static members, it's a constructor of a class, and for instance members, it's a prototype object of a class.
  • Name of member
  • Index of parameters in the list of function parameters
  • Note: The parameter decorator can only be used to monitor whether a method's parameters are passed in.
// Parametric Decorator
class GreeterParam{
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  @validate
  greeter(@required name: string){
    return "Hello, " + name + ", " + this.greeting;
  }
}

// @ The required decorator adds metadata entities to mark parameters as necessary. @ The validate decorator wraps the green method in a function to validate the function parameters before calling the original function
import "reflect-metadata";
const requiredMetadataKey = Symbol('required');
function required(target: Object, propertyKey: string | symbol, parameterIndex: number){
  let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>){
  let method = descriptor.value;
  descriptor.value = function(){
    let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, 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);
  }
}

let greetParam = new GreeterParam('world');
let paramStr = greetParam.greeter('hahaha');
console.log(paramStr)

Posted by russlock on Fri, 06 Sep 2019 08:23:19 -0700