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 memberspropertyKey
: 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 memberspropertyKey
: Attribute namedescriptor
Attributes 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 memberspropertyKey
: Attribute namedescriptor
Attributes 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 memberspropertyKey
: 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