Decorate code with TypeScript decorators

Decorators allow programmers to write meta information to introspect code. The best use case for decorators is cross-cutting concerns-aspect-oriented programming.

Aspect-oriented programming (AOP) is a programming paradigm that allows us to separate crosscutting concerns, thereby achieving the goal of increasing the degree of modularity. It can add additional behavior (notification) to existing code without modifying the code itself.

@log// 类装饰器

classPerson {

constructor(private firstName: string, private lastName: string) {}



@log// 方法装饰器

getFullName() {

return`${this.firstName} ${this.lastName}`;

}

}



const person = newPerson('Mohan', 'Ram');

person.getFullName();

The code above shows how declarative the decorator is. Below we will introduce the details of the decorator:

  1. What is a decorator? Its purpose and type

  2. Decorator's signature

  3. Method decorator

  4. Attribute decorator

  5. Parameter decorator

  6. Accessor decorator

  7. Class decorator

  8. Decorator factory

  9. Meta information reflection API

  10. Conclusion

What is a decorator? Its purpose and type

Decorators are special declarations that can be attached to class, method, accessor, property, and parameter declarations.

Decorators use @expressionthe form, which expressionmust be able to calculations for function calls at run time, including decorative statement information.

It serves to add meta-information to existing code in a declarative way.

The decorator type and its execution priority are

  1. Class decorator-priority 4 (object instantiation, static)

  2. Method decorator-priority 2 (object instantiation, static)

  3. Accessor or attribute decorator-priority 3 (object instantiation, static)

  4. Parameter decorator-priority 1 (object instantiation, static)

Note that if the decorator is applied to the parameters of the class constructor, the priority of different decorators is: 1. Parameter decorator, 2. Method decorator, 3. Accessor or parameter decorator, 4. Constructor parameter decorator , 5. Class decorator.

// 这是一个装饰器工厂——有助于将用户参数传给装饰器声明

function f() {

console.log("f(): evaluated");

returnfunction (target, propertyKey: string, descriptor: PropertyDescriptor) {

console.log("f(): called");

}

}



function g() {

console.log("g(): evaluated");

returnfunction (target, propertyKey: string, descriptor: PropertyDescriptor) {

console.log("g(): called");

}

}



class C {

@f()

@g()

method() {}

}



// f(): evaluated

// g(): evaluated

// g(): called

// f(): called

We see that the above code, fand greturned another function (decorator function). fAnd gcalled a decorator factory.

Decorator Factory helps users to pass parameters that can be used by decorators.

We can also see that the calculation order is from the top down , the execution order is from the bottom up .

Decorator's signature

declare type ClassDecorator =

<TFunctionextendsFunction>(target: TFunction) => TFunction | void;

declare type PropertyDecorator =

(target: Object, propertyKey: string | symbol) => void;

declare type MethodDecorator = <T>(

target: Object, propertyKey: string | symbol,

descriptor: TypedPropertyDescriptor<T>) =>

TypedPropertyDescriptor<T> | void;

Method decorator

From the above signature, we can see that the method decorator function has three parameters:

  1. target  -the prototype of the current object, that is to say, assuming that Employee is an object, then target is Employee.prototype

  2. propertyKey  -the name of the method

  3. descriptor  -the attribute descriptor of the method, ie Object.getOwnPropertyDescriptor(Employee.prototype,propertyKey)

exportfunction logMethod(

target: Object,

propertyName: string,

propertyDescriptor: PropertyDescriptor): PropertyDescriptor {

// target === Employee.prototype

// propertyName === "greet"

// propertyDesciptor === Object.getOwnPropertyDescriptor(Employee.prototype, "greet")

const method = propertyDesciptor.value;



propertyDesciptor.value = function (...args: any[]) {

// 将 greet 的参数列表转换为字符串

constparams = args.map(a => JSON.stringify(a)).join();

// 调用 greet() 并获取其返回值

const result = method.apply(this, args);

// 转换结尾为字符串

const r = JSON.stringify(result);

// 在终端显示函数调用细节

console.log(`Call: ${propertyName}(${params}) => ${r}`);

// 返回调用函数的结果

return result;

}

return propertyDesciptor;

};



class Employee {

constructor(private firstName: string, private lastName: string

) {}



@logMethod

greet(message: string): string {

return`${this.firstName} ${this.lastName} says: ${message}`;

}

}



const emp = newEmployee('Mohan Ram', 'Ratnakumar');

emp.greet('hello');

The code above should be self-explanatory-let's see what the compiled JavaScript looks like.

"use strict";

var __decorate = (this && this.__decorate) ||

function (decorators, target, key, desc) {

// 函数参数长度

var c = arguments.length



/**

* 处理结果

* 如果仅仅传入了装饰器数组和目标,那么应该是个类装饰器。

* 否则,如果描述符(第 4 个参数)为 null,就根据已知值准备属性描述符,

* 反之则使用同一描述符。

*/



var r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc;



// 声明存储装饰器的变量

var d;



// 如果原生反射可用,使用原生反射触发装饰器

if (typeofReflect === "object" && typeofReflect.decorate === "function") {

r = Reflect.decorate(decorators, target, key, desc)

}

else {

// 自右向左迭代装饰器

for (var i = decorators.length - 1; i >= 0; i--) {

// 如果装饰器合法,将其赋值给 d

if (d = decorators[i]) {

/**

* 如果仅仅传入了装饰器数组和目标,那么应该是类装饰器,

* 传入目标调用装饰器。

* 否则,如果 4 个参数俱全,那么应该是方法装饰器,

* 据此进行调用。

* 反之则使用同一描述符。

* 如果传入了 3 个参数,那么应该是属性装饰器,可进行相应的调用。

* 如果以上条件皆不满足,返回处理的结果。

*/

r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r

}

}

};



/**

* 由于只有方法装饰器需要根据应用装饰器的结果修正其属性,

* 所以最后返回处理好的 r

*/

return c > 3 && r && Object.defineProperty(target, key, r), r;

};



varEmployee = /** @class */ (function () {

functionEmployee(firstName, lastName) {

this.firstName = firstName;

this.lastName = lastName;

}

Employee.prototype.greet = function (message) {

returnthis.firstName + " " + this.lastName + " says: " + message;

};



// typescript 调用 `__decorate` 辅助函数,

// 以便在对象原型上应用装饰器

__decorate([

logMethod

], Employee.prototype, "greet");

returnEmployee;

}());

var emp = newEmployee('Mohan Ram', 'Ratnakumar');

emp.greet('hello');

Let us begin to analyze Employee function - the constructor initialization nameparameters and greetmethods, to join prototype.

__decorate([logMethod], Employee.prototype, "greet");

This is a generic method automatically generated by TypeScript, which handles decorator function calls based on the decorator type and corresponding parameters.

This function helps introspection method invocation and paves the way for developers to handle crosscutting concerns such as logging , memorization, and application configuration .

In this example, we only printed the function call and its parameters and responses.

Note that reading __decoratemethods detailed notes can understand its internal mechanisms.

Attribute decorator

The attribute decorator function has two parameters:

  1. target  -the prototype of the current object, that is to say, assuming that Employee is an object, then target is Employee.prototype

  2. propertyKey  -the name of the property

function logParameter(target: Object, propertyName: string) {

// 属性值

let _val = this[propertyName];



// 属性读取访问器

const getter = () => {

console.log(`Get: ${propertyName} => ${_val}`);

return _val;

};



// 属性写入访问器

const setter = newVal => {

console.log(`Set: ${propertyName} => ${newVal}`);

_val = newVal;

};



// 删除属性

if (deletethis[propertyName]) {

// 创建新属性及其读取访问器、写入访问器

Object.defineProperty(target, propertyName, {

get: getter,

set: setter,

enumerable: true,

configurable: true

});

}

}



classEmployee {

@logParameter

name: string;

}



const emp = newEmployee();

emp.name = 'Mohan Ram';

console.log(emp.name);

// Set: name => Mohan Ram

// Get: name => Mohan Ram

// Mohan Ram

In the above code, we introspect the accessibility of attributes in the decorator. The following is the compiled code.

varEmployee = /** @class */ (function () {

functionEmployee() {

}

__decorate([

logParameter

], Employee.prototype, "name");

returnEmployee;

}());

var emp = newEmployee();

emp.name = 'Mohan Ram'; // Set: name => Mohan Ram

console.log(emp.name); // Get: name => Mohan Ram

Parameter decorator

The parameter decorator function has three parameters:

  1. target  -the prototype of the current object, that is to say, assuming that Employee is an object, then target is Employee.prototype

  2. propertyKey  -the name of the parameter

  3. index  -position in the parameter array

function logParameter(target: Object, propertyName: string, index: number) {

// 为相应方法生成元数据键,以储存被装饰的参数的位置

const metadataKey = `log_${propertyName}_parameters`;

if (Array.isArray(target[metadataKey])) {

target[metadataKey].push(index);

}

else {

target[metadataKey] = [index];

}

}



classEmployee {

greet(@logParameter message: string): string {

return`hello ${message}`;

}

}

const emp = newEmployee();

emp.greet('hello');

In the above code, we collected the index or position of all decorated method parameters, and added the prototype of the object as metadata. The following is the compiled code.

// 返回接受参数索引和装饰器的函数

var __param = (this && this.__param) || function (paramIndex, decorator) {

// 该函数返回装饰器

returnfunction (target, key) { decorator(target, key, paramIndex); }

};



varEmployee = /** @class */ (function () {

functionEmployee() {}

Employee.prototype.greet = function (message) {

return"hello " + message;

};

__decorate([

__param(0, logParameter)

], Employee.prototype, "greet");

returnEmployee;

}());

var emp = newEmployee();

emp.greet('hello');

Before seen similar __decoratefunction, __paramthe function returns a package parameter decorator decorator.

As we can see, when the parameter decorator is called, its return value is ignored. This means that calls __parama function, the return value will not be used to override parameter values.

This is why the parameter decorator does not return .

Accessor decorator

Accessors are nothing but read and write accessors for attributes in class declarations.

Accessor decorators are applied to accessor attribute descriptors and can be used to observe, modify, and replace accessor definitions.

function enumerable(value: boolean) {

returnfunction (

target: any, propertyKey: string, descriptor: PropertyDescriptor) {

console.log('decorator - sets the enumeration part of the accessor');

descriptor.enumerable = value;

};

}



classEmployee {

private _salary: number;

private _name: string;



@enumerable(false)

get salary() { return`Rs. ${this._salary}`; }



set salary(salary: any) { this._salary = +salary; }



@enumerable(true)

get name() {

return`Sir/Madam, ${this._name}`;

}



set name(name: string) {

this._name = name;

}



}



const emp = newEmployee();

emp.salary = 1000;

for (let prop in emp) {

console.log(`enumerable property = ${prop}`);

}

// salary 属性不在清单上,因为我们将其设为假

// output:

// decorator - sets the enumeration part of the accessor

// decorator - sets the enumeration part of the accessor

// enumerable property = _salary

// enumerable property = name

In the above example, we defined two accessor nameand salary, and set whether the behavior of its inclusion in the list, pursuant to the decision by the decorator object. nameIt will be included in the list, but salarywill not.

Note: TypeScript not allow simultaneous decoration single-member districts getand setaccess control. Instead, all member decorators must be applied to the first designated accessor (based on document order). This is because the decoration is applied to property descriptor, a combination of the attribute descriptor getand setaccess devices, rather than separately applied to each statement.

Below is the compiled code.

function enumerable(value) {

returnfunction (target, propertyKey, descriptor) {

console.log('decorator - sets the enumeration part of the accessor');

descriptor.enumerable = value;

};

}



varEmployee = /** @class */ (function () {

functionEmployee() {

}

Object.defineProperty(Employee.prototype, "salary", {

get: function () { return"Rs. " + this._salary; },

set: function (salary) { this._salary = +salary; },

enumerable: true,

configurable: true

});

Object.defineProperty(Employee.prototype, "name", {

get: function () {

return"Sir/Madam, " + this._name;

},

set: function (name) {

this._name = name;

},

enumerable: true,

configurable: true

});

__decorate([

enumerable(false)

], Employee.prototype, "salary", null);

__decorate([

enumerable(true)

], Employee.prototype, "name", null);

returnEmployee;

}());

var emp = newEmployee();

emp.salary = 1000;

for (var prop in emp) {

console.log("enumerable property = " + prop);

}

Class decorator

Class decorators are used in class constructors and can be used to observe, modify, and replace class definitions.

exportfunction logClass(target: Function) {

// 保存一份原构造器的引用

const original = target;



// 生成类的实例的辅助函数

function construct(constructor, args) {

const c: any = function () {

return constructor.apply(this, args);

}

c.prototype = constructor.prototype;

returnnew c();

}



// 新构造器行为

const f: any = function (...args) {

console.log(`New: ${original['name']} is created`);

return construct(original, args);

}



// 复制 prototype 属性,保持 intanceof 操作符可用

f.prototype = original.prototype;



// 返回新构造器(将覆盖原构造器)

return f;

}



@logClass

classEmployee {}



let emp = newEmployee();

console.log('emp instanceof Employee');

console.log(emp instanceofEmployee); // true

The above named decorator declares a originalvariable, its value is set to be decorated class constructor.

Then the statement called constructauxiliary functions. This function is used to create an instance of a class.

Next, we created a named fvariable, the variable will be used as the new constructor. This function calls the original constructor and prints the instantiated class name in the console. This is where we add extra behavior to the original constructor .

The prototype of the original constructor is copied to fensure that when creating a new instance of Employee, instanceofthe effect of the operator is as expected.

Once the new constructor is ready, we return to it to complete the implementation of the class constructor.

After the new constructor is ready, the class name is printed in the console each time an instance is created.

The compiled code is as follows.

varEmployee = /** @class */ (function () {

functionEmployee() {

}

Employee = __decorate([

logClass

], Employee);

returnEmployee;

}());

var emp = newEmployee();

console.log('emp instanceof Employee');

console.log(emp instanceofEmployee);

In the compiled code, we noticed two differences:

  1. As you can see, __decorate there are two parameters passed  , the decorator array and the constructor function.

  2. __decorate The return value used by the TypeScript compiler  to override the original constructor.

This is why the class decorator must return a constructor .

Decorator factory

Since each decorator has its own call signature, we can use the decorator factory to generalize decorator calls.

import { logClass } from'./class-decorator';

import { logMethod } from'./method-decorator';

import { logProperty } from'./property-decorator';

import { logParameter } from'./parameter-decorator';



// 装饰器工厂,根据传入的参数调用相应的装饰器

exportfunction log(...args) {

switch (args.length) {

case3: // 可能是方法装饰器或参数装饰器

// 如果第三个参数是数字,那么它是索引,所以这是参数装饰器

iftypeof args[2] === "number") {

return logParameter.apply(this, args);

}

return logMethod.apply(this, args);

case2: // 属性装饰器

return logProperty.apply(this, args);

case1: // 类装饰器

return logClass.apply(this, args);

default: // 参数数目不合法

thrownewError('Not a valid decorator');

}

}



@log

classEmployee {

@log

private name: string;



constructor(name: string) {

this.name = name;

}



@log

greet(@log message: string): string {

return`${this.name} says: ${message}`;

}

}

Meta information reflection API

Meta information reflection API (for example Reflect) can be used to organize meta information in a standard way.

"Reflection" means that the code can detect other codes (or itself) in the same system.

Reflection is useful in combination / dependency injection, runtime type assertions, testing, and other usage scenarios.

import"reflect-metadata";



// 参数装饰器使用反射 api 存储被装饰参数的索引

exportfunction logParameter(target: Object, propertyName: string, index: number) {

// 获取目标对象的元信息

const indices = Reflect.getMetadata(`log_${propertyName}_parameters`, target, propertyName) || [];

indices.push(index);

// 定义目标对象的元信息

Reflect.defineMetadata(`log_${propertyName}_parameters`, indices, target, propertyName);

}



// 属性装饰器使用反射 api 获取属性的运行时类型

exportfunction logProperty(target: Object, propertyName: string): void {

// 获取对象属性的设计类型

var t = Reflect.getMetadata("design:type", target, propertyName);

console.log(`${propertyName} type: ${t.name}`); // name type: String

}





classEmployee {

@logProperty

private name: string;



constructor(name: string) {

this.name = name;

}



greet(@logParameter message: string): string {

return`${this.name} says: ${message}`;

}

}

The above code uses the reflect-metadata library. Among them, we use the design key of the reflection element information (for example:) design:type. There are currently only three:

  • The type meta information uses the meta information key  design:type.

  • The meta information key is used for the  parameter type meta informationdesign:paramtypes .

  • The meta-information key is used to return the meta-information  design:returntype.

With reflection, we can get the following information at runtime:

  • The entity name .

  • Entity type .

  • The interface implemented by the entity .

  • The name and type of the entity constructor parameter .

Conclusion

  • Decorators are  nothing more than functions that help introspect code, annotate and modify classes and properties at design time .

  • Yehuda Katz proposes to add decorator features to the ECMAScript 2016 standard: tc39 / proposal-decorators

  • We can pass the parameters provided by the user to the decorator through the decorator factory .

  • There are 4 kinds of decorators: class decorators, method decorators, attribute / accessor decorators, and parameter decorators.

  • The Meta Information Reflection API  helps to add meta information to objects in a standard way, as well as to obtain design type information at runtime .

Published 117 original articles · 69 praises · 10,000+ views

Guess you like

Origin blog.csdn.net/zsd0819qwq/article/details/105321897