TypeScript learning-19 decorator

Decorator

introduce

With the introduction of classes in TypeScript and ES6, in some scenarios, we need additional features to support labeling or modifying classes and their members. Decorators provide a way for us to add annotations on class declarations and members through metaprogramming syntax. Decorators in Javascript are currently in the second stage of solicitation, but have been supported as an experimental feature in TypeScript.

be careful Decorator is an experimental feature that may change in future versions.

To enable the experimental decorator feature, you must be on the command line or tsconfig Enable the experimentalDecorators compiler option in JSON:

Command line:

tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

Decorator

Decorator is a special type of declaration that can be attached to class declarations, methods, accessors, properties or parameters. The decorator uses the form of @ expression. Expression must be a function after evaluation. It will be called at run time, and the decorated declaration information will be passed in as parameters.

For example, there is a @ sealed decorator. We will define the sealed function as follows:

function sealed(target) {
    // do something with "target" ...
}
be careful  There is a more detailed example of decorator in the following section.

Decorator factory

If we want to customize how a decorator is applied to a declaration, we have to write a decorator factory function. Decorator factory is a simple function that returns an expression for the decorator to call at run time.

We can write a decorator factory function in the following way:

function color(value: string) { // This is a decorator factory
    return function (target) { //  This is a decorator
        // do something with "target" and "value"...
    }
}
be careful  There is a more detailed example in the method decorator section below.

Decorator assembly

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
  • Write on multiple lines:
@f
@g
x

When multiple decorators are applied to a declaration, they are evaluated in a manner similar to composite functions. In this model, when f and G are combined, the result of combination (f ∘ g)(x) is equal to f(g(x)).

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

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

If we use decorator factories, we can observe their evaluation order through 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()
    @g()
    method() {}
}

The following results will be printed in the console:

f(): evaluated
g(): evaluated
g(): called
f(): called

Decorator evaluation

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

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

Class decorator

The class decorator is declared before the class declaration (immediately following 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 declare's class).

The class decorator expression is called as a function at run time, and the constructor of the class is its only parameter.

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

Note that if you want to return a new constructor, you must pay attention to dealing with the original prototype chain. This will not be done for you in the decorator call logic at run time.

The following is an example of using the class decorator (@ sealed), which is applied to the Greeter class:

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

We can define @ sealed decorator as follows:

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

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

The following 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"));

Method decorator

The method decorator declaration precedes (immediately following) the declaration of a method. It is applied to the property descriptor of the method 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 declare's class).

The method decorator expression will be called as a function at runtime, passing in the following three parameters:

  1. For static members, it is the constructor of the class, and for instance members, it is the prototype object of the class.

  2. Name of the member.

  3. Property descriptor of the member.

    Note   if the target version of code output is less than ES5, the attribute descriptor will be undefined.

If the method decorator returns a value, it will be used as the property descriptor of the method.

be careful  If the target version of code output is less than ES5 The return value is ignored.

The following is an example of a method decorator (@ enumerable) applied to the methods of the Greeter class:

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

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

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;
    };
}

Here @ enumerable(false) is a decorator factory. When the decorator @ enumerable(false) is called, it modifies the enumerable property of the property descriptor.

Accessor decorator

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

be careful  TypeScript It is not allowed to decorate the of one member at the same time get and set Accessor. Instead, all decorations of a member must be applied to the first accessor in the document order. This is because when the decorator is applied to an attribute descriptor, it combines get and set Accessors, not separately declared.

The accessor decorator expression will be called as a function at runtime, passing in the following three parameters:

  1. For static members, it is the constructor of the class, and for instance members, it is the prototype object of the class.
  2. Name of the member.
  3. Property descriptor of the member.
    Note   if the target version of code output is less than ES5, the Property Descriptor will be undefined.

If the accessor decorator returns a value, it will be used as the property descriptor of the method.

be careful  If the target version of code output is less than ES5 The return value is ignored.

The following is an example of using the accessor decorator (@ configurable) to apply to the members 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; }
}

We can define the @ configurable decorator through the following function declaration:

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

Attribute decorator

A property decorator declaration precedes (immediately following) a property declaration. Attribute decorators cannot be used in declaration files (. d.ts) or in any external context (such as declare's class).

The expression of the property decorator will be called as a function at runtime, passing in the following two parameters:

  1. For static members, it is the constructor of the class, and for instance members, it is the prototype object of the class.

  2. Name of the member.

    Note that   the attribute descriptor will not be passed into the attribute decorator as a parameter, which is related to how TypeScript initializes the attribute decorator. At present, there is no way to describe an instance property when defining a member of a prototype object, and there is no way to monitor or modify the initialization method of a property. The return value is also ignored. The descriptor can only be used to monitor whether an attribute in a class is declared.

We can use it to record the metadata of this attribute, 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);
    }
}

Then define the @ format decorator and 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);
}

This @ format("Hello,% s") decorator is a decorator factory. When @ format("Hello,% s") is called, it adds a piece of metadata of this attribute through reflect. In the reflect metadata library Metadata function. When getFormat is called, it reads the metadata of the format.

be careful  This example needs to be used reflect-metadata Library. View metadata to understand reflect-metadata Library for more details.

Parameter decorator

The parameter decorator declaration precedes (immediately following) a parameter declaration. Parameter decorators apply to class constructors or method declarations. Parameter decorators cannot be used in declaration files (. d.ts), overloads, or other external contexts (such as declare's classes).

The parameter decorator expression will be called as a function at runtime, passing in the following three parameters:

  1. For static members, it is the constructor of the class, and for instance members, it is the prototype object of the class.

  2. Name of the member.

  3. The index of the parameter in the function parameter list.

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

The return value of the parameter decorator is ignored.

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

class Greeter {
    greeting: string;

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

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

Then we define the @ 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);
    }
}

@The required decorator adds metadata entities to mark parameters as required@ The validate decorator wraps the greet method in a function and validates the function parameters before calling the original function.

be careful  This example uses reflect-metadata Library. View metadata to understand reflect-metadata More information about libraries.

metadata

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

You can install this library through npm:

npm i reflect-metadata --save

TypeScript supports generating metadata for declarations with decorators. You need to be on the command line or tsconfig Enable the emitDecoratorMetadata compiler option in JSON.

Command Line:

tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

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

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 (!(value instanceof type)) {
            throw new TypeError("Invalid type.");
        }
        set(value);
    }
}

TypeScript compiler can use @ reflect The metadata decorator injects type information at the design stage. You can think of it as the following 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; }
}
be careful  Decorator metadata is an experimental feature and may change destructively in later versions( breaking changes). 

Keywords: Javascript Front-end TypeScript

Added by Timma on Mon, 07 Feb 2022 07:37:06 +0200