Share what you learned from studying Decorators

The concept is introduced

Decorators are one of the most powerful features TypeScript provides, enabling us to extend the functionality of classes and methods in a clean, declarative way. Decorators are currently a phase 2 proposal in JavaScript, but have become popular in the TypeScript ecosystem and are being used by major open source projects such as Angular.

I’m working on a project using Angular8 and have a lot of exposure to Decorators, so I wrote an article about Decorators over the weekend in the hope of helping students who are learning about Decorators. Without further ado, let’s get down to business.

Start using decoratorsDecorators

Json to enable the experimentalDecorators compiler option:

Command-line: TSC –target ES5 –experimentalDecorators

tsconfig.json:

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

Let’s first clarify two concepts:

  • Right now decorators are essentially a function,@expressionIs actually a grammatical sugar,expressionThe evaluation must also be a function, which is called at run time with the embellished declaration information passed in as an argument.
  • JavaScriptIn theClassIt’s also a grammar candy.

For example, we declare a Class in Javascript:

Class Animal {
    eat() {
        console.log('eat food')}}Copy the code

The Animal class is equivalent to the following:

function Animal() {}
Object.defineProperty(Animal.prototype, 'eat', {
    value: function() { console.log('eat food'); },
    enumerable: false,
    configurable: true,
    writable: true
});

Copy the code

Class decorator

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

function setDefaultDesc(constructor: Function){
    constructor.prototype.desc = 'Class decorator Properties'
}

@setDefaultDesc
class Animal {
  name: string;
  desc: string;
  constructor() {
    this.name = 'dog'; }}let animal= new Animal();

console.log(animal.desc) // 'Class decorator Properties'
Copy the code

Here is an example of using overloaded functions.

functionclassDecorator<T extends {new(... args:any[]):{}}>(constructor:T) {return class extends constructor {
        newProperty = "new property";
        desc = "override";
    }
}

@classDecorator
class Animal {
    property = "property";
    desc: string;
    constructor(m: string) {
        this.desc = m;
    }
}

console.log(new Animal("world")); // Animal: {property: "property", desc: "override", newProperty: "new property" }
Copy the code

The meaning of this code is: be classDecorator decoration class if there is no newProperty or desc attribute, will increase the value of the corresponding attributes and corresponding, if there is this property will override the value of the attribute.

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.

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

  1. targetThe prototype of the current object (if Employee is an object, i.eEmployee.prototype)
  2. PropertyKey Method name
  3. descriptorMethod property descriptorObject.getOwnPropertyDescriptor(Employee.prototype, propertyKey)
function logMethod(
    target: Object,
    propertyName: string,
    propertyDesciptor: PropertyDescriptor): PropertyDescriptor {
    
    // target === Employee.prototype
    // propertyName === "greet"
    // propertyDesciptor === Object.getOwnPropertyDescriptor(Employee.prototype, "greet")
    
    const method = propertyDesciptor.value;

    propertyDesciptor.value = function(... Args: any[]) {// Convert argument list to string const params = args.map(a => json.stringify (a)).join(); Const result = method.apply(this, args); // Call the method and return the result const result = method.apply(this, args); Const r = json.stringify (result); // Display function Call details in the console console.log(' Call:${propertyName}(${params}) = >${r}`); // Returns the result of the callreturn result;
    }
    return propertyDesciptor;
};

class Employee {

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

    @logMethod
    greet(message: string): string {
        return `${this.firstName} ${this.lastName} : ${message}`;
    }

}

const emp = new Employee('March style'.'Flowers on a Stranger');
emp.greet('Flowers in March'); // return: 'Flowers on March: Flowers on March'

Copy the code

Accessor decorator

Accessors are just the getter and setter parts of a property in a class declaration. Accessor decorators are declared before accessors are declared. Accessor decorators are property descriptors applied to accessors that can be used to observe, modify, or replace the definition of accessors.

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

  1. targetThe prototype of the current object (if Employee is an object, i.eEmployee.prototype)
  2. PropertyKey Method name
  3. descriptorMethod property descriptorObject.getOwnPropertyDescriptor(Employee.prototype, propertyKey)

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

function configurable(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;
    }

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

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

Attribute decorator

The attribute decorator function takes two arguments:

  1. Target Indicates the prototype of the current object
  2. PropertyKey Specifies the name of the property
function logParameter(target: Object, propertyName: string) {// The value of the propertylet_val = target[propertyName]; // Property get method const getter = () => {console.log(' get:${propertyName}= >${_val}`);
        return_val; }; / / propertiessetConst setter = newVal => {console.log(' Set:${propertyName}= >${newVal}`); _val = newVal; }; // Delete attributes.if(delete target[propertyName]) {// Use getter and setter to create a new property object.defineProperty (target, propertyName, {get: getter,set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class Employee {

    @logParameter
    name: string;

}

const emp = new Employee();

emp.name = 'Flowers on a Stranger'; // Set: name => console.log(emp.name); // Get: name =Copy the code

Parameter decorator

The argument decorator function takes three arguments:

  1. Target Indicates the prototype of the current object
  2. PropertyKey Method name
  3. The position of the index parameter in the parameter array
function logParameter(target: Object, propertyName: string, index: number) {// Generate metadata for the corresponding method // Keep the position of the modified Parameter const metadataKey = 'log_${propertyName}_parameters`;
    if (Array.isArray(target[metadataKey])) {
        target[metadataKey].push(index);
    }
    else {
        target[metadataKey] = [index];
    }
}

class Employee {
    greet(@logParameter message: string): string {
        return `hello ${message}`;
    }
}
const emp = new Employee();
emp.greet('hello');

Copy the code

In the above code example, the target instance emp is Employee, the propertyName value is greet, and the index value is 0.

Decoration factory

Let’s imagine a scenario where we need several decorators to print the names of some properties, the class itself, methods, and parameters in a class.


import { logClass } from './class-decorator';
import { logMethod } from './method-decorator';
import { logProperty } from './property-decorator';
import { logParameter } from './parameter-decorator'; // Assuming we already have the above decorators, we should do the following.function log(... args) { switch (args.length) {case3: // Can be a method decorator or a parameter decoratorif (typeof args[2] === "number") {// If the third argument is number then its index is its argument decoratorreturn logParameter.apply(this, args);
            }
            return logMethod.apply(this, args);
        case2: // Attribute decoratorreturn logProperty.apply(this, args);
        case1: // class decoratorreturn logClass.apply(this, args); Default: // throw new Error('Not a valid decorator'); }} @log
class Employee {

    @log
    private name: string;

    constructor(name: string) {
        this.name = name;
    }

    @log
    greet(@log message: string): string {
        return `${this.name} says: ${message}`; }}Copy the code

A decorator factory is a simple function that returns a decorator of one type.

Metadata Reflection API

@Reflect.metadata('name'.'A')
class A {
  @Reflect.metadata('hello'.'world')
  public hello(): string {
    return 'hello world'
  }
}

Reflect.getMetadata('name', A) // 'A'
Reflect.getMetadata('hello', new A()) // 'world'
Copy the code
  • Relfect Metadata, which can be used to add custom information to a class via decorators
  • This information is then extracted by reflection
  • You can also add this information through reflection

Relfect is used for reflection. It allows a running application to examine, or “self-examine,” itself and directly manipulate internal properties and methods. Reflection is a concept that has been widely used in Java/c# and many other languages

Here’s a quick example:

function logParameter(target: Object, propertyName: string, index: number) {// getMetadata from the target Object const indices = reflect.getmetadata (' log_${propertyName}_parameters`, target, propertyName) || []; indices.push(index); // Define the metadata as the target object reflect.definemetadata (' log_${propertyName}_parameters`, indices, target, propertyName); } // The property decorator uses the reflection API to get the runtime type of the propertyexport function logProperty(target: Object, propertyName: string): void {var t = reflect.getMetadata ("design:type", target, propertyName);
    console.log(`${propertyName} type: ${t.name}`); // name type: String
}


class Employee {
  
    @logProperty
    private name: string;
    
    constructor(name: string) {
        this.name = name;
    }

    greet(@logParameter message: string): string {
        return `${this.name} says: ${message}`; }}Copy the code

In the example above, we used the reflection metadata design key [Design: Type]. There are only three:

  • Type metadata uses metadata keysdesign:type
  • Parameter type metadata uses metadata keysdesign:paramtypes
  • Return type metadata using metadata keysdesign:returntype

conclusion

This article introduces class decorators, method decorators, accessor decorators, property decorators, parameter decorators, decorator factories, and metadata Reflection. It is also a summary of my learning process. Every week will continue to update different technologies, students who like can like and follow, we make progress together. If you want to learn some technology, please leave a message in the comments section. I will try my best to write what you are interested in.