TypeScript tutorial: Lesson 28 – Decorator


Since ES6, Class was appeared in javascript:

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

Accompanying that, during development, there will be many cases where we find it necessary to add information or edit classes or class components (property, method …), especially at run-time. At this time, Decorators, Metadata Reflection appear and provide methods that allow us to add annotations or meta-data for classes, properties, methods …

Decorator is promised to be one of javascript (since ES 7), but typescript has come ahead with support from the beginning Decorator (which is a feature used quite a lot in C #, a language also run by Microsoft. develope). In this article, we’ll learn how to use decorators in typescript.

Decorator
Decorator can be considered as a special declaration syntax, never stand alone but always attached to a declaration of class, method, property or accessor. Decorator is written in the form @expression, with expression pointing to a function that will be called at runtime, with the task of changing or adding to the decorate object.

In pure javascript before ES6, the concept of decorators also came to be as “functional composition” – enclosing a function with another. For example, when we are close to logging the operation of a function, we can create a decorator function that wraps the function to perform.

function doBusinessJob(arg) {
  console.log('do my job');
}

function logDecorator(job) {
  return function() {
    console.log('start my job');
    const result = job.apply(this, arguments);
    return result;
  }
}

const logWrapper = logDecorator(doBusinessJob);

The function wrapped in logWrapper is called exactly the same as doBusinessJob, with the difference that it does additional logging before the business is done.

doBusinessJob();
// do my job
logDecorator();
// start my job
// do my job

Similar to the above, in Typescript, @expression is actually a function:

function expression(target) {
   // do something with target 
}

To customize how a decorator is applied to its target, or pass params to the decorator, we can use the decorator factory – which is actually a function, which returns an expression that will be called by the decorator at run-time.

function customDecorator(value: integer) {   // => decorator factory
  return function (target): void {           // => decorator
     // do something with decorated target and input value
  }
}

In Typescript, there are five types of decorators:

  • class decorator
  • method decorator
  • property decorator
  • accessor decorator
  • parameter decorator

Class decorator
A Class decorator is defined immediately before the class definition.

@logCreate
class Animal {}

Corresponding to it, the decorator function will take 1 param – constructor of the decorate class. For example:

function logCreate(Class:any):any {
    return function(...args:any[]) {
      console.log('Object created with args: ', args)
      return new Class(...args)
    }
}

The above decorator will log every time a new instance of the class is initialized.

@logCreate
class Animal {
  constructor(name:string) {}
}

const dog = new Animal("Tom") // Object created with args:  [ 'Tom' ]

Notice that the params Class of logCreate is the input class. So, what if we want to pass more parameters to the decorator?

=> Use decorator factory:

function logCreate(...additionalParams:any[]):any {
    return function (Class:any):any {
      return function(...args:any[]):any {
        console.log('Object created with args: ', args, '; and receive another params: ', additionalParams)
        return new Class(...args)
      }    
    }
}

@logCreate('customParam1','customParam2')
class Animal {
    constructor(name:string) {}
}

const cat = new Animal("Tom") // Object created with args:  [ 'Tom' ] ; and receive another params:  [ 'customParam1', 'customParam2' ]

Method decorator

Unlike Class decorator, Method decorator is declared with 3 params:

The parameters will be:

  • target: Either the constructor function of the class for a static member, or the prototype of the class for an instance member.
  • the name of the member is decorate (for the decorator method, it’s the name of the method).
  • The Property Descriptor of the method.

Return to the logDecorator example at the beginning of this article. Now, we can write a lot more concise.

function logDecorator(target: any, key: string, descriptor: any) {
    const originalMethod = descriptor.value
  
    descriptor.value = function(...args: any[]) {
      console.log('start my job')
      return originalMethod.apply(this, args)
    }
  
    return descriptor
}
  
class MyClass {
  
  @logDecorator
  doBusinessJob(...args:any[]) {
    console.log('do my job')
  }

} 
new MyClass().doBusinessJob("Job name")
// start my job
// do my job

Use Method decorator with parameters like @myDecorator(param):

function enumerable(value){
    console.log('run decorator enumerable')
    return function(target:any, propertyKey: string, descriptor:PropertyDescriptor):any{
        descriptor.enumerable = value        
    }
}
 
class Demo {
    
    name: string    
    
    constructor(name:string){
        this.name = name
    }

    @enumerable(false)
    myJob() {
        console.log('my name:'+this.name)
    }
    
}

var demo = new Demo('Bean')
for(var i in demo){
    console.log(i)
}

Result:

run decorator enumerable
name[/js]

<strong>Decorator order</strong>

We can completely apply multiple decorators to an object.

1

Meanwhile, decorators will be executed sequentially from top to bottom. At the same time, the output of the decorator (the edited object) of the bottom decorator will become the input to the upper decorator.

1

Result:
1deco1(): evaluated
deco2(): evaluated
deco2(): called
deco1(): called

Accessor decorator

Similar to method decorator, accessor decorator is used to decorate the accessor of a certain property.

function Enumerable(target:any, propertyKey: string, descriptor:PropertyDescriptor):any{
    descriptor.enumerable = true
    return descriptor  
}
function notEnumerable(target:any, propertyKey: string, descriptor:PropertyDescriptor):any{
    descriptor.enumerable = false
    return descriptor  
}

class Demo {
    
    private _name: string    
    private _id: number
  
    constructor(name:string){
        this._name = name
    }

    @Enumerable
    get name(): string {
      return this._name
    }
    
    set name(name:string) {
        this._name = name
    }

    @notEnumerable
    get id(): number{
        return this._id
    }

    set id(id){
        this._id = id
    }
}

var demo = new Demo('Bean')
demo.id = 10
for(var i in demo){
    console.log(i)
}

Result:

_name
_id
name

Note: With the accessor decorator, we specify a decorate for which accessor (get or set) is written first.

@modify
get name(): string {
}

// No @modify here!
set name(input: string){
}

Use Accessor decorator with parameters like @myDecorator(param):

function enumerable(value){
    return function(target:any, propertyKey: string, descriptor:PropertyDescriptor):any{
        descriptor.enumerable = value        
    } 
}
class Demo {
    
    private _name: string    
    private _id: number
  
    constructor(name:string){
        this._name = name
    }

    @enumerable(true)
    get name(): string {
      return this._name
    }
    
    set name(name:string) {
        this._name = name
    }

    @enumerable(false)
    get id(): number{
        return this._id
    }

    set id(id){
        this._id = id
    }
}

var demo = new Demo('Bean')
demo.id = 10
for(var i in demo){
    console.log(i)
}

Result:

_name
_id
name

Property decorator

Unlike method decorator and accessor decorator , a property decorator will have only 2 input parameters: target and propertyKey. The Property Descriptor is not passed as an argument of a property decorator.

function myPropertyDecorator(target:any, propertyKey: string):any{
    //decorator code
}

Example:

function myPropertyDecorator(target:any, propertyKey: string):any{
    console.log(target, propertyKey)
}

class Demo2 {
    @myPropertyDecorator
    name: string   
    @myPropertyDecorator 
    id: number
  
    constructor(id:number,name:string){
        this.id = id
        this.name = name
    }
}

Result:

{} name
{} id

This decorator has many interesting uses: we can use it together with Metadata to modify and interfere with the object's property. (example at the next post)

Parameter decorator

The Parameter decorator is defined immediately before a parameter - it can be a parameter of a function or a constructor of the Class.

Parameter decorator takes 3 params as input:

  • Either the constructor function of the class for a static member, or the prototype of the class for an instance member.
  • the name of the param is decorate.
  • param order in the list of params of the parent function.

Parameter decorator is often used to check for the existence of params in the function, and is often used in conjunction with either decorator method or accessor decorator.

Example:

function myParameterDecorator(targetClass:any, paramName: string, paramOrder: number):any{
    console.log(targetClass, paramName, paramOrder)
}

class Demo3 {
    
    name: string   
    id: number
  
    constructor(@myParameterDecorator id:number,@myParameterDecorator name:string){
        this.id = id
        this.name = name
    }
}

Result:

[class Demo3] undefined 1
[class Demo3] undefined 0

Enable experimental support for decorators
To enable experimental support for decorators, you must enable the experimentalDecorators compiler option either on the command line or in your tsconfig.json:

Command Line: tsc --target ES5 --experimentalDecorators

tsconfig.json:

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

tsc -b tsconfig.json

1 Comment

Leave a Reply