Decorators in Typescript
Tags: typescript, decorators, nestjs, webdev
Published: 2023-01-04 00:51:16

Decorators are similar to annotations in JAVA or the attributes in C#. They provide additional definitions for a class, property, or function. Decorators are proposed as a part of ECMAScript 7. Decorators are useful to inject additional capabilities into existing definitions without the need to rewrite or duplicate the existing code.

To use decorator we need to enable “experimentalDecorators” to true in tsconfig.json. Otherwise, we will be prompted with the below compile time error when compiling typescript tsc <filename.ts>

error TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the ‘experimentalDecorators’ option in your ‘tsconfig’ or ‘jsconfig’ to remove this warning.

I came across this when developing microservice using Nestjs and it becomes really important to understand them as it is widely used across in Nestjs ecosystem. e.g. @UseGuards in Nestjs is an example of a method | class decorator.

Decorators can be viewed as simple functions with input parameters that provide definitions of a class, property, or function at runtime. Note the usage of @<decorator>.

Decorators are called during the definition stage and not in the instance creation stage. To understand better let's look at this example

`function simpleDecorator(target:Function)
{
   console.log("I am from simple decorator");
}
@simpleDecorator
export class ClassType
{
  constructor()
  {
    console.log("I am from ctor");
  }
}
let instance = new ClassType();

output: 
I am from simple decorator
I am from ctor`

“I am from simple decorator” is printed in the console before “I am from ctor” We can see that decorators are invoked before instance creations. in fact, if we create multiple instances of classType we will see decorators are called only once when the classType is defined.

Ok, But why we are having the parameter of decorator as a function why not another type in the above example? That is because the Class in TS is a Function in Javascript. If we don’t have the type as the function and have it as string type then we will get an error “Argument of type ‘type ClassType’ is not assignable to parameter of type ‘String’.”

Can we have multiple decorators applied to a class, property, or function? Yes.

What if we have to pass parameters to the decorator? For this, we have to use a decorator factory. The decorator factory returns a decorator function e.g.

//Decorator factory return a decorator function 
function decoratorFactory(paramters: any[])
{
  return function(ctor:Function)
  {
  console.log(`Data passed onto decorator:${paramters}`);
  }
}
@decoratorFactory(["param 1", "param 2", "param n"])
export class TargetClass{
}

Types of Decorators

Class decorators.

Method decorators.

Property decorators.

Parameter decorators.

Class decorator

All the above examples we have seen so far are of class decorators. With class-level decorators, we can inject property through prototypes e.g.

function Component(targetClass:Function)
{
  //Injecting porperty via decorator.
  targetClass.prototype.FrmDecoraator = "prototype property set by decorator";
  console.log(targetClass.name);

  //Accessing the name property of the target class Function.
  console.log((<any>targetClass).StaticProperty); 
}
//class decorator
@Component
export class TargetClass
{ 
  static StaticProperty:string ="static property";
  constructor()
  {
    console.log("I am from ctor");
  }
}
let instanceOfTargetClass= new TargetClass();
//Injected prototype from decorator function into TargetClass
console.log((<any>instanceOfTargetClass).FrmDecoraator);
Property Decorators :

Property decorators are applied to the properties within a class. it is a function that accepts the prototype of the class and the property key as its input parameters. In the below e.g. we can access the static property values but not the instance property value as decorators are created before instance creation.

function propetryDecorators(target:any,propertyKey:string)
{
    console.log(target.constructor.name);
    console.log(`property Key name :${propertyKey}`);
    /*Property value is undefined for non static property
     as it is not yet initialized, but we can access the value of static property here. */
    console.log(`property value :${target[propertyKey]}`);
}
export class propetryDecoratorExample
{
 @propetryDecorators
  name:string  = "property inside class"

  @propetryDecorators
  static staticProperty:string ="Static property";
}
let instance = new propetryDecoratorExample();
Method decorators:

As the name suggests these decorators are applied to the method in the class. Method decorators are functions with three parameters (class prototype, method name, and the optional param descriptor about the method). e.g. AuthGaurd() in Nest is an example of a method decorator. AuthGaurd is triggered first before the method invocation.

Parameter decorators

To decorate the parameters of a method. .e.g @Body(), @Param() in Nest js routes are of parameter decorator type. Parameter decorators can be used to check if the parameter is part of the method. To probe further on the parameter say the type of the parameter we need to install a third-party tool npm install reflect-metadata --save-dev and enable "emitDecoratorMetadata": true in tsconfig.json file.

import 'reflect-metadata';

function paramDecorator(target:any,methodName:string, parameterIndex:number)
{
console.log(`target: ${target.constructor.name}`);
console.log(`methodName : ${methodName}`);
console.log(`parameterIndex : ${parameterIndex}`);

//using reflection to probe more on the parameter.
let designType = Reflect.getMetadata(
    "design:type", target, methodName);
    console.log(`designType: ${designType}`);

    let designParamTypes = Reflect.getMetadata(
    "design:paramtypes", target, methodName);
    console.log(`paramtypes : ${designParamTypes}`);
    if(designParamTypes !== String)
    {
       console.log("not string");
    }
}
class ClassType
{
 Method(@paramDecorator param:number ,   nonParameterDecorator:string)
 {}
}
After enabling the "emitDecoratorMetadata": true, Generated .js file will have info about the __decorate __metadata.

__decorate([
    __param(0, paramDecorator),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Number, String]),
    __metadata("design:returntype", void 0)
], ClassType.prototype, "Method", null);

References for further reading on this topic

Packt Mastering Type script second edition by Mr. Nathan Rozentals

https://docs.nestjs.com/custom-decorators

https://javascript.plainenglish.io/master-the-typescript-keyof-type-operator-bf7f18865a8b

Thank you for reading. Until we meet again I Wish you the very best.
Vivek