In JavaScript, the decorator feature has not yet been officially released and is currently in Stage 2; in TypeScript, decorators are experimental and the experimentalDecorators configuration must be set to true to enable support for them. Use decorators to add additional behavior to a class or its methods without modifying the basic functionality of the class.

 Basic syntax of decorators


Decorators are functions that can be used to decorate classes, their methods, accessors, attributes, and method arguments with the syntax @expression, where expression must be a function that is called at program initialization to rewrite the decorated target or add additional functionality inside the function. The same target can be decorated with multiple decorators, which can be written in such a way that they can be placed on top of each other in rows or side by side on the same line. The example code is as follows:

 
@decoratorA
@decoratorB
target
 
@decoratorA @decoratorB target


In both the above writeups, the decorators are called in the same order as: decoratorA(decoratorB(target)). To decorate the data attribute of a class with multiple decorators, the code is as follows:

function NonWritable<T>(target: T, propertyKey: keyof T) {
    console.log('NonWritable')

    Object.defineProperty(target, propertyKey, {
        writable: false
    });
}

function NonEnumerable<T>(target: T, propertyKey: keyof T) {
    console.log('NonEnumerable')

    Object.defineProperty(target, propertyKey, {
        enumerable: false
    });
}

class User {
    @NonEnumerable
    @NonWritable
    name?: string
}


The above code applies two decorators to the name attribute, NonWritable and NonEnumerable, with NonWritable called first and NonEnumerable called later.


A concept related to decorators is introduced here, namely: decorator factories. A decorator factory is a function that returns a decorator with the call syntax: @decoFactory(… .args). Decorator factories are used more often than decorator functions in project development because you can pass custom arguments to them for greater reusability. Next, use the decorator factory to rewrite the previous example of the decorator, the results are as follows:

function writable(bool: boolean) {
    console.log('writable factory')
    return function <T>(target: T, propertyKey: keyof T) {
        console.log('writable decorator')

        Object.defineProperty(target, propertyKey, {
            writable: bool
        });
    }
}

function enumerable(bool: boolean) {
    console.log('enumerable factory')
    return function <T>(target: T, propertyKey: keyof T) {
        console.log('enumerable decorator')

        Object.defineProperty(target, propertyKey, {
            enumerable: bool
        });
    }
}

class User {
    @enumerable(false)
    @writable(false)
    name?: string
}


Running the above code in TS Playground gives the following result:

 class decorator


A class decorator acts on a class to modify or replace the class definition, the constructor is the only parameter it accepts, a class decorator can have no return value, if it does then it must be a constructor, the original constructor is replaced with the returned constructor, the usage is as follows:

type Ctor = new (...args: any[]) => Object;

function withLog<T extends Ctor>(ctor: T){
 
    return class extends ctor {
        log() {
            console.log('Reported')
        }
    }
}

@withLog
class User {
    name?: string
}

const user= new User();
 
(user as any).log()


When @withLog is placed on the User class, the generic type parameter T of withLog is equivalent to typeof User.

 method decorator


The method decorator acts on the static and instance methods of a class, and can be used to modify or replace method definitions, accepting the following three parameters:


  1. If the target of the method decorator decoration is a static method, the first argument is the class (or constructor) of the static member; if the target of the method decorator decoration is an instance method, the first argument is the prototype of the class (or constructor).
  2. 方法名

  3. The property descriptor of the method. The third argument is undefined if the TS is compiled with a target less than ES5.


A method decorator can have no return value, if it does then it must be an access descriptor and the original constructor is replaced with the returned access descriptor. The method decorator usage is as follows:

function readonly<T>(target: T, name: string, descriptor: PropertyDescriptor){
    descriptor.writable = false
}

function vPrefix(target: typeof User, name: string, descriptor: PropertyDescriptor) {
    return {
        ...descriptor,
        value: () => {
            return `v${descriptor.value()}`
        }
    }
}

class User {
    @vPrefix
    static getVersion() {
        return '1.0.0'
    }

    @readonly
    printName() {
        return 'default name'
    }
}


In the above code, the decorator readonly acts on the instance method of User, so the first parameter of readonly is User.prototype; the decorator vPrefix acts on the static method of User, so the first parameter of VPrefix is User.

 Accessor Decorator


Accessor decorators act on the static and instance accessors of a class, and can be used to modify or replace the definition of an accessor, accepting the same arguments as method decorators. Accessors have getter/setter, theoretically, you can provide separate decorators for getter and setter of the accessor, but this is not encouraged, it is recommended to provide a decorator for one of the getter/setter, and then define get and set in the property descriptor returned by the decorator. usage is as follows:

function toUpperCase<T>(target: T, name: string, descriptor: PropertyDescriptor) {
    return {
        ...descriptor,
        get: function(): string{
	 
            const val: string = descriptor!.get!.call(this)
            return val.toUpperCase()
        },

        set: function(val: string) {
	     // todo someThing
            descriptor!.set!.call(this, val)
        }
    }
}

class User {
    firstName: string = 'bella'
    lastName: string = 'Q'

    @toUpperCase
    get fullName() {
        return this.firstName + ' ' + this.lastName
    }

    set fullName(fullName: string) {
        const [firstName, lastName] = fullName.split(' ')
        this.firstName = firstName
        this.lastName = lastName
    }
}

const user = new User()
console.log(user.fullName)

 Property Decorator


The attribute decorator acts on the static and instance attributes of a class and accepts only two arguments, the first of which is the same as the first argument of the method decorator, and the second of which is the name of the attribute. It has no return value.


Decorator functions are executed after the class is defined, when the instance has not been created, so it is not possible for the instance to be used as an argument to the decorator function. When the attribute decorator acts on the instance attribute, the first parameter of the decorator function is the prototype of the class, so that the attribute descriptor of the instance attribute can not be modified in the attribute decorator, if you must act on the attribute decorator on the instance attribute, then you need to use the curve to save the country, the following is a simple example.

function toUpper(target: any, key: string) {
   target.__cache__  = {}
   const cache: { [attr: string]: string } = {}
   const desc: PropertyDescriptor = {
       enumerable: true,
       configurable: true,
       get() {
           return cache[key]?.toUpperCase()
       },
       set(val: string) {
            cache[key] = val
       }
   }
   Object.defineProperty(target.__cache__, key, desc)
}

class Base {
    __cache__: { [attr: string]: string } = {}
}

class User extends Base {
    @toUpper
    private name?: string;

    constructor(name: string) {
        super()
        this.setName(name)
    }
    getName() {
        return Object.getPrototypeOf(this).__cache__['name']
    }
    setName(val: string){
        Object.getPrototypeOf(this).__cache__['name'] = val
    }
}


Static properties are located on the class and are determined after the class is defined, so property decorators can directly modify the property descriptors of static properties. The following is an example demonstrating property decorators acting on static properties.

function toUpper(target: any, key: string) {
   const cache: {[key: string]: any} = {}
   const desc = Object.getOwnPropertyDescriptor(target, key)
   cache[key] = desc?.value

   Object.defineProperty(target, key, {
       enumerable: true,
       configurable: true,
       get() {
         const value = cache[key]
         return typeof value === 'string' ? value.toUpperCase() : value
       },
       set(value: any) {
        cache[key] = value
       }
   })

   return target
}

class User {
    @toUpper
    static version: string = 'v2.3.4'
}


When a property decorator acts on a static property, the first parameter of the decorator function is the class of the static member, and the static property is located directly on the class, so you can get the access descriptor of the static property with Object.getOwnPropertyDescriptor, and you can modify the access descriptor of the static property with Object.defineProperty defineProperty.

 parameter decorator


Parameter decorators act on the parameters of constructors and methods. A parameter decorator accepts three parameters, its first two parameters are the same as the first two parameters of a method decorator, the third parameter indicates the index number of the parameter, and the return value of the parameter decorator is ignored. The following is an example of using a parameter decorator:

function toNumber(target: any, name: string, index: number) {
    Reflect.defineMetadata(name+'_toNumber', true, target)
}

class User {
    age: number | string
    constructor(@toNumber age: number | string) {
        this.age = age
    }

    getAge() {
        const toNumber = Reflect.getMetadata('undefined_toNumber', User)
        if (toNumber && typeof this.age === 'string') {
            return Number(this.age)
        } else {
            return this.age
        }
    }
}

By lzz

Leave a Reply

Your email address will not be published. Required fields are marked *