Original Mohan Ram, licensed translation by New Frontend.

Decorators allow programmers to write meta-information introspection code. The best use of decorators is for crosscutting concerns — section-oriented programming.

Section-oriented programming (AOP) is a programming paradigm that allows us to separate crosscutting concerns in order to achieve an increased degree of modularity. It can add additional behavior (notifications) to existing code without modifying the code itself.

@log // Class decorator
class Person {
  constructor(private firstName: string.private lastName: string) {}

  @log // Method decorator
  getFullName() {
    return `${this.firstName} ${this.lastName}`; }}const person = new Person('Mohan'.'Ram');
person.getFullName();
Copy the code

The code above shows how declarative decorators can be. Here are the details of the decorator:

  1. What is a decorator? Its purpose and type
  2. Decorator signature
  3. Method decorator
  4. Attribute decorator
  5. Parameter decorator
  6. Accessor decorator
  7. Class decorator
  8. Decoration 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.

The decorator uses the form @expression, where expression must be evaluated as a function to be invoked at run time, including decorator declaration information.

It acts as a declarative way to add meta-information to existing code.

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 property decorator — Priority 3 (object instantiation, static)
  4. Parameter Decorator — Priority 1 (object instantiation, static)

Note that if decorators are applied to arguments to class constructors, the priorities of the different decorators are: 1. Parameter decorator, 2. Method decorator, 3. Accessor or parameter decorator, 4. Constructor parameter decorator, 5. Class decorator.

// This is a decorator factory -- it helps to pass user parameters to decorator declarations
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()
  method() {}
}

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

We see that in the code above, f and g return another function (the decorator function). F and G are called decorator factories.

Decorator factories help users pass in parameters that are available to decorators.

We can also see that the order of evaluation is top down, and the order of execution is bottom up.

Decorator signature

declare type ClassDecorator =
  <TFunction extends Function>(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;
Copy the code

Method decorator

From the signature above, we can see that the method decorator function takes three arguments:

  1. target— The prototype of the current object, that is, assuming Employee is the object, then target isEmployee.prototype
  2. PropertyKey – The name of the method
  3. descriptor— The property descriptor of the method, i.eObject.getOwnPropertyDescriptor(Employee.prototype, propertyKey)
export function 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[]) {
    // Convert the greet argument list to a string
    const params = args.map(a= > JSON.stringify(a)).join();
    // Call greet() and get the return value
    const result = method.apply(this, args);
    // Convert the end to a string
    const r = JSON.stringify(result);
    // Display the function call details on the terminal
    console.log(`Call: ${propertyName}(${params}) = >${r}`);
    // Returns the result of calling the function
    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 = new Employee('Mohan Ram'.'Ratnakumar');
emp.greet('hello');

Copy the code

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

"use strict";
var __decorate = (this && this.__decorate) ||
    function (decorators, target, key, desc) {
        // The length of the function argument
        var c = arguments.length

        If only the decorator array and target are passed in, it should be a class decorator. * Otherwise, if the descriptor (parameter 4) is null, the property descriptor is prepared based on the known value, * otherwise the same descriptor is used. * /

        var r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc;
        
        // Declare the variable that stores the decorator
        var d;

        // If native reflection is available, use native reflection trigger decorator
        if (typeof Reflect= = ="object" && typeof Reflect.decorate === "function") {
            r = Reflect.decorate(decorators, target, key, desc)
        }
        else {
            // Iterate over the decorator from right to left
            for (var i = decorators.length - 1; i >= 0; i--) {
                // If the decorator is valid, assign it to d
                if (d = decorators[i]) {
                    /** * If only the array of decorators and the target are passed in, it should be the class decorator, and the target calls the decorator. Otherwise, if all four arguments are present, it should be a method decorator, and the call should be made accordingly. * Otherwise, use the same descriptor. * If three arguments are passed in, it should be an attribute decorator that can be called accordingly. * If none of the above conditions are met, the result of the processing is returned. * /
                    r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r
                }
            }
        };

        /** * Since only the method decorator needs to modify its properties based on the result of applying the decorator, the processed r */ is returned
        return c > 3 && r && Object.defineProperty(target, key, r), r;
    };

var Employee = /** @class */ (function () {
    function Employee(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    Employee.prototype.greet = function (message) {
        return this.firstName + "" + this.lastName + " says: " + message;
    };

    // typescript calls the '__pipeline' helper function,
    // To apply the decorator to the object prototype
    __decorate([
        logMethod
    ], Employee.prototype, "greet");
    returnEmployee; } ());var emp = new Employee('Mohan Ram'.'Ratnakumar');
emp.greet('hello');
Copy the code

Let’s start by analyzing the Employee function — the constructor initializes the name parameter and greet method and adds them to the prototype.

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

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

This function helps to introspect method calls and paves the way for developers to deal with crosscutting concerns such as logging, memorization, application configuration, and so on.

In this example, we simply print the function call with its parameters and response.

Note that you can understand the internal mechanism by reading the detailed annotations in the __pipeline method.

Attribute decorator

The property decorator function takes two arguments:

  1. target— The prototype of the current object, that is, assuming Employee is the object, then target isEmployee.prototype
  2. PropertyKey – The name of the property
function logParameter(target: Object, propertyName: string) {
    / / property values
    let _val = this[propertyName];

    // Property read accessor
    const getter = (a)= > {
        console.log(`Get: ${propertyName}= >${_val}`);
        return _val;
    };

    // Write accessors to properties
    const setter = newVal= > {
        console.log(`Set: ${propertyName}= >${newVal}`);
        _val = newVal;
    };

    // Delete attributes
    if (delete this[propertyName]) {
        // Create a new property and its read accessors and write accessors
        Object.defineProperty(target, propertyName, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true}); }}class Employee {
    @logParameter
    name: string;
}

const emp = new Employee();
emp.name = 'Mohan Ram';
console.log(emp.name);
// Set: name => Mohan Ram
// Get: name => Mohan Ram
// Mohan Ram
Copy the code

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

var Employee = /** @class */ (function () {
    function Employee() {
    }
    __decorate([
        logParameter
    ], Employee.prototype, "name");
    returnEmployee; } ());var emp = new Employee();
emp.name = 'Mohan Ram'; // Set: name => Mohan Ram
console.log(emp.name); // Get: name => Mohan Ram
Copy the code

Parameter decorator

The parameter decorator function takes three arguments:

  1. target— The prototype of the current object, that is, assuming Employee is the object, then target isEmployee.prototype
  2. PropertyKey – The name of the parameter
  3. Index – Position in the parameter array
function logParameter(target: Object, propertyName: string, index: number) {
    // Generate metadata keys for the corresponding methods to store the positions of the decorated parameters
    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, we collected the index or location of all the decorated method parameters as a prototype for adding metadata to the object. Here is the compiled code.

// Returns the function that accepts the parameter index and decorator
var __param = (this && this.__param) || function (paramIndex, decorator) {
  // This function returns the decorator
  return function (target, key) { decorator(target, key, paramIndex); }};var Employee = /** @class */ (function () {
    function Employee() {}
    Employee.prototype.greet = function (message) {
        return "hello " + message;
    };
    __decorate([
        __param(0, logParameter)
    ], Employee.prototype, "greet");
    returnEmployee; } ());var emp = new Employee();
emp.greet('hello');
Copy the code

Like the __decorate function you saw earlier, the __param function returns a decorator that wraps the parameter decorator.

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

This is why the parameter decorator does not return.

Accessor decorator

Accessors are simply read accessors and write accessors for attributes in a class declaration.

Accessor decorators are property descriptors applied to accessors that can be used to observe, modify, or replace the definition of accessors.

function enumerable(value: boolean) {
    return function (
      target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log('decorator - sets the enumeration part of the accessor');
        descriptor.enumerable = value;
    };
}

class Employee {
    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 = new Employee();
emp.salary = 1000;
for (let prop in emp) {
    console.log(`enumerable property = ${prop}`);
}
// Salary property is not on the list because we set it to false
// output:
// decorator - sets the enumeration part of the accessor
// decorator - sets the enumeration part of the accessor
// enumerable property = _salary
// enumerable property = name
Copy the code

In the above example, we defined two accessors, name and Salary, and determined the behavior of the object by setting them to be included in the list via the decorator. Name will be listed, salary will not.

Note: TypeScript does not allow you to decorate both get and SET accessors for a single member. Instead, decorators for all members must be applied to the first specified accessor (in document order). This is because decorators are applied to property descriptors, which combine get and set accessors, rather than to each declaration separately.

Here is the compiled code.

function enumerable(value) {
    return function (target, propertyKey, descriptor) {
        console.log('decorator - sets the enumeration part of the accessor');
        descriptor.enumerable = value;
    };
}

var Employee = /** @class */ (function () {
    function Employee() {}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 = new Employee();
emp.salary = 1000;
for (var prop in emp) {
    console.log("enumerable property = " + prop);
}
Copy the code

Class decorator

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

Export function logClass(target: function) {const original = target; // Construct (constructor, args) {const c: const c: const c: const c: const any = function () { return constructor.apply(this, args); } c.prototype = constructor.prototype; return new c(); } // new constructor behavior const f: any = function (... args) { console.log(`New: ${original['name']} is created`); return construct(original, args); } // copy the prototype property and keep the intanceof operator available. // return the new constructor (to override the original constructor) } @logClass class Employee {} let emp = new Employee(); console.log('emp instanceof Employee'); console.log(emp instanceof Employee); // trueCopy the code

The decorator above declares a variable named Original and sets its value to the class constructor being decorated.

We then declare an helper function named Construct. This function is used to create instances of the class.

We next create a variable named f that will be used as the new constructor. This function calls the original constructor and prints the name of the instantiated class on the console. This is where we add additional behavior to the original constructor.

The prototype of the original constructor is copied to f to ensure that the instanceof operator behaves as expected when creating a new instanceof Employee.

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

With the new constructor in place, the class name is printed on the console each time an instance is created.

The compiled code looks like this.

var Employee = /** @class */ (function () {
    function Employee() {
    }
    Employee = __decorate([
        logClass
    ], Employee);
    returnEmployee; } ());var emp = new Employee();
console.log('emp instanceof Employee');
console.log(emp instanceof Employee);
Copy the code

In the compiled code, we notice two differences:

  1. As you can see, pass__decorateTakes two arguments, a decorator array and a constructor function.
  2. Used by the TypeScript compiler__decorateTo override the original constructor.

This is why the class decorator must return a constructor.

Decoration factory

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

import { logClass } from './class-decorator';
import { logMethod } from './method-decorator';
import { logProperty } from './property-decorator';
import { logParameter } from './parameter-decorator';

// Decorator factory, which calls the corresponding decorator based on the argument passed in
export function log(. args) {
    switch (args.length) {
        case 3: // Can be a method decorator or a parameter decorator
            // If the third argument is a number, it is an index, so this is the argument decorator
            if typeof args[2= = ="number") {
                return logParameter.apply(this, args);
            }
            return logMethod.apply(this, args);
        case 2: // Attribute decorator
            return logProperty.apply(this, args);
        case 1: // Class decorator
            return logClass.apply(this, args);
        default: // The number of arguments is invalid
            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

Meta information reflection API

Meta information reflection apis (such as Reflect) can be used to organize meta information in a standard way.

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

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

import "reflect-metadata";

// The parameter decorator uses the reflection API to store the index of the decorated parameter
export function logParameter(target: Object, propertyName: string, index: number) {
    // Get the meta information of the target object
    const indices = Reflect.getMetadata(`log_${propertyName}_parameters`, target, propertyName) || [];
    indices.push(index);
    // Define the target object's meta information
    Reflect.defineMetadata(`log_${propertyName}_parameters`, indices, target, propertyName);
}

// The property decorator uses the reflection API to get the runtime type of the property
export function logProperty(target: Object, propertyName: string) :void {
    // Get the design type of the object property
    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

The above code uses the reflect-metadata library. Here, we use design keys that reflect meta information (for example: Design: Type). There are only three:

  • Type meta-informationMeta key useddesign:type.
  • Parameter Type Meta informationMeta key useddesign:paramtypes.
  • Returns type meta informationMeta key useddesign: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 simply functions that help you introspect your code, annotate and modify classes and properties at design time.
  • Yehuda Katz proposes to add a decorator feature to the ECMAScript 2016 standard: TC39 /proposal-decorators
  • We can pass user-supplied parameters to the decorator through the decorator factory.
  • There are four types of decorators: class decorator, method decorator, property/accessor decorator, and parameter decorator.
  • The meta-information reflection API helps to add meta-information to objects in a standard way and to retrieve design type information at run time.

I put all of the code examples in this article into the Mohanramphp /typescript-decorators Git repository. Thanks for reading!

Title: Alex Loup

Other content recommendation

  • Six popular Open source apps for macOS
  • 22 hot open source projects for iOS development
  • NPM practical tips you may not know