Welcome to wechat public account: Front Reading Room

introduce

With the introduction of classes in TypeScript and ES6, there are some scenarios where we need additional features to support annotation or modification of classes and their members. Decorators provide a way for us to add annotations to class declarations and members through metaprogramming syntax. Decorators in Javascript are currently in the second phase of the call for proposals, but are already supported as an experimental feature in TypeScript.

Note that decorators are an experimental feature and may change in future releases.

To enable the experimental decorator feature, you must enable the experimentalDecorators compiler option either on the command line or in tsconfig.json:

The command line:

tsc --target ES5 --experimentalDecorators
Copy the code

tsconfig.json:

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

A decorator

A decorator is a special type of declaration that can be attached to a class declaration, method, accessor, property, or parameter. The decorator uses the form @expression. Expression must be evaluated as a function, which is called at run time with the decorated declaration information passed in as an argument.

For example, with a @sealed decorator, we would define sealed as follows:

function sealed(target) {
    // do something with "target" ...
}
Copy the code

Note the class decorator section below for a more detailed example.

Decoration factory

If we want to customize how a decorator is applied to a declaration, we have to write a decorator factory function. A decorator factory is a simple function that returns an expression to be invoked by the decorator at run time.

We can write a decorator factory function as follows:

function color(value: string) { // This is a decorator factory
    return function (target) { // This is a decorator
        // do something with "target" and "value"...}}Copy the code

Note a more detailed example in the method decorator section below.

Decorator combination

Multiple decorators can be applied to a declaration at the same time, as in the following example:

Write on the same line:

@f @g x
Copy the code

Write on multiple lines:

@f
@g
x
Copy the code

When multiple decorators are applied to a declaration, they are evaluated like composite functions. In this model, the CVD result (F ∘ G)(x) is the same as F (G (x) when it is CVD f and G.

Similarly, in TypeScript, when multiple decorators are applied to a declaration, the following steps are performed:

  1. Evaluate the decorator expression from top to bottom.
  2. The result of the evaluation is called as a function from the bottom up.

If we use a decorator factory, we can observe the order in which they are evaluated by using the following example:

function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called"); }}function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): called"); }}class C {
    @f(a)@g(a)method(){}}Copy the code

The following results are printed on the console:

f(): evaluated
g(): evaluated
g(): called
f(): called
Copy the code

Decorator evaluation

Decorators on different declarations in a class will be applied in the following order:

  1. A parameter decorator, followed by a method decorator, accessor decorator, or property decorator, is applied to each instance member.
  2. Parameter decorators, followed by method decorators, accessor decorators, or property decorators, are applied to each static member.
  3. Parameter decorators are applied to constructors.
  4. Class decorators are applied to classes.

Class decorator

Class decorators are declared before the class declaration (right next to the class declaration). Class decorators are applied to class constructors and can be used to monitor, modify, or replace class definitions. Class decorators cannot be used in declaration files (.d.ts) or in any external context (such as a class for declare).

A class decorator expression is called at run time as a function, with the constructor of the class as its only argument.

If the class decorator returns a value, it replaces the class declaration with the provided constructor.

Note that if you return a new constructor, you must take care to handle the original prototype chain. This is not done for you in the decorator call logic at runtime.

The following is an example of using a class decorator (@sealed) in a Greeter class:

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting; }}Copy the code

We can define the @sealed decorator as follows:

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}
Copy the code

When @sealed is executed, it will seal the constructor and prototype of this class. (Note: see Object.seal)

Here is an example of an overloaded constructor.

function classDecorator<T extends {new(... args:any[]) : {}} > (constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override"; }}@classDecorator
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m; }}console.log(new Greeter("world"));
Copy the code

Method decorator

The method decorator declaration precedes the declaration of a method (right next to the method declaration). It is applied to the method’s property descriptor and can be used to monitor, modify, or replace the method definition. Method decorators cannot be used in declaration files (.d.ts), overloads, or any external context (such as a class of Declare).

The method decorator expression is called as a function at run time, passing in the following three arguments:

  1. Constructor of the class for static members and prototype object for instance members.
  2. Member’s name.
  3. Attribute descriptor for a member.

Note that if the code output target version is smaller than ES5, the property descriptor will be undefined.

If a method decorator returns a value, it is used as the property descriptor for the method.

Note that the return value is ignored if the code output target version is smaller than ES5.

Here is an example of a method decorator (@Enumerable) that applies to a method of the Greeter class:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting; }}Copy the code

We can define the @Enumerable decorator with the following function declaration:

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}
Copy the code

The @Enumerable (False) here is a decorator factory. When the decorator @Enumerable (false) is called, it modifies the Enumerable property of the property descriptor.

Accessor decorator

The accessor decorator declaration precedes the accessor declaration (immediately following the accessor declaration). Accessor decorators are applied to accessor property descriptors and can be used to monitor, modify, or replace an accessor’s definition. Accessor decorators cannot be used in declaration files (.d.ts), or in any external context (such as a class of Declare).

Note that TypeScript does not allow you to decorate both get and set accessors for a member. Instead, all decorators of a member must be applied to the first accessor in the document order. This is because when a decorator is applied to a property descriptor, it combines the GET and set accessors rather than declaring them separately.

Accessor decorator expressions are called as functions at run time, passing in the following three arguments:

  1. Constructor of the class for static members and prototype object for instance members.
  2. Member’s name.
  3. Attribute descriptor for a member

Note that if the code output target version is smaller than ES5, the Property Descriptor will be undefined.

If the accessor decorator returns a value, it is used as the property descriptor for the method.

Note that the return value is ignored if the code output target version is smaller than ES5.

The following is an example of using the accessor decorator (@signals), which works with a member of the Point class:

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

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

    @configurable(false)
    get y() { return this._y; }}Copy the code

The @64x decorator is configured using the following function declaration:

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}
Copy the code

Attribute decorator

The property decorator declaration precedes (immediately after) a property declaration. Property decorators cannot be used in declaration files (.d.ts), or in any external context (such as a class of Declare).

The property decorator expression is called as a function at run time, passing in the following two arguments:

  1. Constructor of the class for static members and prototype object for instance members.
  2. Member’s name.

Note that property descriptors are not passed as arguments to property decorators, depending on how TypeScript initializes property decorators. Because there is currently no way to describe an instance property when defining a member of a prototype object, there is no way to monitor or modify the initialization method of a property. The return value is also ignored. Therefore, attribute descriptors can only be used to monitor whether a class has declared an attribute of a given name.

We can use it to record metadata for this property, as shown in the following example:

class Greeter {
    @format("Hello, %s")
    greeting: string;

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

Then define the @format decorator and the getFormat function:

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);
Copy the code

The @format(“Hello, %s”) decorator is a decorator factory. When @format(“Hello, %s”) is called, it adds a piece of metadata to this property, via the reflect.metadata function in the reflect-metadata library. When getFormat is called, it reads metadata for the format.

Note that this example requires the reflect-metadata library. See metadata for more details on the Reflect-Metadata library.

Parameter decorator

The parameter decorator declaration precedes (immediately after) a parameter declaration. Parameter decorators are applied to class constructors or method declarations. Parameter decorators cannot be used in declaration files (.d.ts), overloads, or other external contexts (such as classes of Declare).

The parameter decorator expression is called as a function at run time, passing in the following three arguments:

  1. Constructor of the class for static members and prototype object for instance members.
  2. Member’s name.
  3. The index of a parameter in the function argument list.

Note that the parameter decorator can only be used to monitor whether a method’s parameters are passed in.

The return value of the parameter decorator is ignored.

The following example defines a parameter decorator (@required) and applies a parameter to the Greeter class method:

class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    @validate
    greet(@required name: string) {
        return "Hello " + name + "," + this.greeting; }}Copy the code

We then define @required and @validate decorators using the following functions:

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); }}Copy the code

The @required decorator adds a metadata entity to mark the parameters as required. The @validate decorator wraps the greet method in a function that validates its arguments before calling the original one.

Note that this example uses the reflect-metadata library. See metadata for more information about the Reflect-Metadata library.

metadata

Some examples use the Reflect-Metadata library to support the experimental metadata API. This library is not yet part of the ECMAScript (JavaScript) standard. However, when decorators are adopted by the official ECMAScript standard, these extensions will also be recommended for adoption by ECMAScript.

You can install this library via NPM:

npm i reflect-metadata --save
Copy the code

TypeScript supports generating metadata for declarations with decorators. You need to enable the emitDecoratorMetadata compiler option either on the command line or in tsconfig.json.

Command Line:

tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
Copy the code

tsconfig.json:

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

When enabled, the type information added at design time can be used at run time, as long as the reflect-metadata library is introduced.

As shown in the following example:

import "reflect-metadata";

class Point {
    x: number;
    y: number;
}

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }}function validate<T> (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
    let set = descriptor.set;
    descriptor.set = function (value: T) {
        let type = Reflect.getMetadata("design:type", target, propertyKey);
        if(! (valueinstanceof type)) {
            throw new TypeError("Invalid type."); } set(value); }}Copy the code

The TypeScript compiler can inject design-time type information through the @reflect.metadata decorator. You can think of it as the equivalent of TypeScript:

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    @Reflect.metadata("design:type", Point)
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    @Reflect.metadata("design:type", Point)
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }}Copy the code

Note that decorator metadata is an experimental feature and is subject to breaking changes in future releases.

Welcome to wechat public account: Front Reading Room