ts decorator learning summary 2

The 2nd learning decorator record contains practical examples

Use a decorator

tsc --target ES5 --experimentalDecorators

tsconfig.json

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

1. Class decorator

Class decorators are declared before the class declaration (immediately after 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) nor in any external context (such as declare d classes).

A class decorator expression is called at runtime as a function with the class's constructor as its only parameter.

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

Basic usage

function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        //Inherit the decorated class The methods and properties defined here will be added to it
        name = 'custom value'
        //custom add method
        test() {
            return 'custom added methods and' + this.name 
        }
        
    }
}

@classDecorator
class Test {
}

console.log(new Test().test());// Custom added methods and custom values

Example of use

import axios, { AxiosRequestConfig } from "axios";
// class decorator
const requestDecorator = (baseURL) => <T extends { new(...args: any[]): {} }>(constructor: T) => {


    return class extends constructor {
		//post request encapsulation
        post(url, data = {}, config?: AxiosRequestConfig) {
            return axios({
                method: 'POST',
                url: url,
                data: data,
                baseURL: baseURL,
                ...config,
            })
        }
        //get request wrapper
        get(url, data = {}, config?: AxiosRequestConfig) {
            return axios({
                method: 'GET',
                url: url,
                params: data,
                baseURL: baseURL,
                ...config,
            })

        }

        //Inherit the decorated class The methods and properties defined here will be added to it
    }
}


@requestDecorator(' https://www.baidu.com/')
class ClassTest {
    [x: string]: any; 
    async getData() { 
        let res = await this.get('/s', {
            wd: 'axios'
        })
        console.log(res.statusText)//OK
        return res
    }
 


}

new ClassTest().getData()

2. Method decorator

Method decorators are declared before (immediately) a method declaration. It is applied to the method's property descriptor and can be used to monitor, modify or replace the method definition. Method decorators cannot be used in declaration files ( .d.ts), in overloads or in any external context (such as declare d classes).

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

1.For static members, it is the class constructor, and for instance members, it is the class's prototype object.
2.member's name.
3.The attribute descriptor of the member.

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

Notice  If the code output target version is less than ES5 The return value is ignored.

Basic usage

const methodsDecorator = (name: string) => (target: any, methodsName: string, desc: PropertyDescriptor) => {
    const _value = desc.value; 
    //function coverage
    desc.value = function () {
            //Here you can define the action you want 
            _value.apply(this, arguments) 
    } 
} 


class Test {
    @methodsDecorator()
    test(num) {
      
    }
}


console.log(new Test().test(123))

Error catch example

const error = 
(params = `function name error\n Parameter array: parms\n error message:error\n`) =>
 (_, methodsName: any, desc: PropertyDescriptor) => {
    const newValue = function (...parms: any[]) { // Override the wrapper function and add a try catch 
        try {
            return desc.value.apply(this, parms) // Borrow the encapsulated function and return the result, remember to write return
        } catch (error) { 
            const errorText = params
                .replace('name', methodsName)
                .replace('parms', JSON.stringify(parms))
                .replace('error', JSON.stringify( error instanceof Error ? { message: error.message, name: error.name } : error ))
            console.log(errorText) 
           // Error in function test 
		//Parameter array: ["parameter 1", "parameter 2"] 
       //Error message:{"message":"Error capture example","name":"Error"} 
        }
    }
    return {
        value: newValue
    }
}



class Test {  
    @error() 
    test() { 
        throw new Error('Error catch example')

     }
}


new Test().test('parameter 1','parameter 2')

3. Accessor Decorator

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

Notice  TypeScript It is not allowed to decorate a member at the same time get and set accessor. Instead, all decorations of a member must be applied to the first accessor in document order. This is because, when the decorator is applied to a property descriptor, it combines get and set accessors, rather than being declared separately.

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

1.For static members, it is the class constructor, and for instance members, it is the class's prototype object.
2.member's name.
3.The attribute descriptor of the member.

If the accessor decorator returns a value, it is used as the method's property descriptor.

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;
    }
	//set to not configurable
    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}


let res = new Point(1,2)

res.x = 12 // You are not allowed to change this property


4. Attribute Decorator

A property decorator declaration precedes (immediately) a property declaration. Property decorators cannot be used in declaration files (.d.ts), or in any external context (such as declare classes).

Property decorator expressions are called at runtime as functions, passing in the following 2 arguments:

1.For static members, it is the class constructor, and for instance members, it is the class's prototype object.
2.member's name.

1. The return value is ignored.
2. Therefore, the property descriptor can only be used to monitor whether a property of a certain name is declared in the class.

Basic usage

//property decorator
const attributeDecorator = () => (target: any, key: string) => {
    //Here you can do a very complex set of calculations and set default properties
    target[key] = 'default properties' 
}


class Point {  
    @attributeDecorator()
    private x: number; 
}

console.log(new Point().x)

5. Parameter decorator

A parameter decorator declaration precedes (immediately) a parameter declaration. Argument decorators are applied to class constructors or method declarations. Parameter decorators cannot be used in declaration files (.d.ts), overloads or other external contexts (such as declare classes).

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

1.For static members, it is the class constructor, and for instance members, it is the class's prototype object.
2.member's name.
3.The index of the parameter in the function parameter list.

The return value of the parameter decorator is ignored.
Parameter decorators must be used in conjunction with method decorators to make sense

Effect

class Test {
    @testMethods()
    test(@func(Number) num, @func(String) str) {

        console.log(typeof num) //Convert parameter to number
        console.log(typeof str) //Convert parameter to string
    }
   
}

new Test().test('123', 22222)

Decorator Definition

import "reflect-metadata";
//parameter decorator
const func = (params: any) => (target: any, method: string, index: number) => { 
    let res = Reflect.getMetadata(method, target) || []
    res.push({ name: params, index: index })
    Reflect.defineMetadata(method, res, target) 
}


//method decorator
const testMethods = (params?: any) => (target: any, methodsName: any, desc: any) => {
    let _value = desc.value;
    let list = Reflect.getMetadata(methodsName, target) 
    desc.value = function () {
        let param = Array.from(arguments) 
        for (const { name, index } of list) {

            if (typeof name === 'function') {
                param[index] = name(param[index])
            } else {
                param[index] = name
            }

        }
        _value.apply(this, param)

    }


}


metadata

1. Metadata is mainly implemented using the reflect-metadata library
2. The main function is to pass data in the class to different decorators for use

npm i reflect-metadata --save

tsconfig.json:

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

Introducing reflect-metadata in the global js will mount Reflect to the global

import "reflect-metadata"; 

1.Reflect.getMetadata(“design:paramtypes”, target, key) //Get the function parameter type
2.Reflect.getMetadata("design:returntype", target, key) //return value type
3.Reflect.getMetadata(“design:type”, target, key) // type

Example

function Prop(): PropertyDecorator {
  return (target, key: string) => {
    const type = Reflect.getMetadata('design:type', target, key);
    console.log(`${key} type: ${type.name}`);
    // other...
  };
}

class SomeClass {
  @Prop()
  public Aprop!: string;
}

customize

In addition to obtaining type information, it is often used to customize metadataKey and obtain its value at the right time. Examples are as follows:

function classDecorator(): ClassDecorator {
  return target => {
    // Define metadata on the class, the key is `classMetaData`, the value is `a`
    Reflect.defineMetadata('classMetaData', 'a', target);
  };
}

function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // Define metadata on the prototype property 'someMethod' of the class, key is `methodMetaData`, value is `b`
    Reflect.defineMetadata('methodMetaData', 'b', target, key);
  };
}

@classDecorator()
class SomeClass {
  @methodDecorator()
  someMethod() {}
}

Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'

Example: Inversion of Control and Dependency Injection

type Constructor<T = any> = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => {};

class OtherService {
  a = 1;
}

@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a);
  }
}

const Factory = <T>(target: Constructor<T>): T => {
  // Get all injected services
  const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // 1

Implementation of Controller and Get

If you are using TypeScript to develop Node applications, I believe you are no stranger to Controller, Get, POST decorators:

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {}
}

These Decorator s are also implemented based on Reflect Metadata. This time, we define metadataKey on the value of descriptor:

const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';

const Controller = (path: string): ClassDecorator => {
  return target => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  }
}

const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  }
}

const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');

Next, create a function that maps out the route:

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);

  // Filter out the methodName of the class
  const methodsNames = Object.getOwnPropertyNames(prototype)
                              .filter(item => !isConstructor(item) && isFunction(prototype[item]));
  return methodsNames.map(methodName => {
    const fn = prototype[methodName];

    // Take out the defined metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
    return {
      route,
      method,
      fn,
      methodName
    }
  })
};

So we can get some useful information:

Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'

mapRoute(new SomeClass());

/**
 * [{
 *    route: '/a',
 *    method: 'GET',
 *    fn: someGetMethod() { ... },
 *    methodName: 'someGetMethod'
 *  },{
 *    route: '/b',
 *    method: 'POST',
 *    fn: somePostMethod() { ... },
 *    methodName: 'somePostMethod'
 * }]
 *
 */

Finally, just bind the route related information to express or koa and it's ok.

References

1.ts official website
2.Deep understanding of TypeScript

Tags: ts

Posted by Kryllster on Thu, 02 Jun 2022 12:15:55 +0530