TypeScript Decorators | Examples | Use Cases

For the following examples to work please make sure you have the following specified in your tsconfig.json:

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

Decorators let you add information and behavior to a class, method, property, or accessor at runtime.

Class Decorator

function smithFamily(constructor:T) {
   return class extends constructor {
       lastName = "Smith"
   }
}

@smithFamily
class Person {
   firstName: string;
   constructor(m: string) {
       this.firstName = m;
   }
}

console.log(new Person("John"));

Output:

class_1 { firstName: 'John', lastName: 'Smith' }

Notice how we've defined a decorator function familyMember that modifies the constructor function of a class to add a lastName property.

This decorator is applied anywhere we include @familyMember before a class definition.

Method Decorator

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 let originalMethod = descriptor.value
 descriptor.value = function(){
   console.log("logging a statement before running function")
   return originalMethod.apply()
 }
}

class SomeClass {
 @log
 someMethod(): any {
   console.log("running someMethod")
 }
}

let myClass = new SomeClass()
myClass.someMethod()

Output

logging a statement before running function
running someMethod

Notice how we first define a decorator function log that we then apply to the definition of someMethod() via @log. Unlike our first example, this function takes 3 arguments:

  • target: The parent class (either the constructor function for a static member or prototype for instance member)
  • propertyKey: The name of the property (in this instance someMethod)
  • descriptor: The PropertyDescriptor for the object

Using the member's PropertyDescriptor we are able to augment the original function with a log statement.

Now we can apply this logging functionality to any class method by adding @log before the definition.

Accessor Decorator

function canEnumerate(val: boolean){
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      descriptor.enumerable = val
  }
}

class SomeClass {
  _x: number;
  _y: number;
  constructor(x: number, y: number){
    this._x = x;
    this._y = y;
  }

  @canEnumerate(true)
  get x(){
    return this._x
  }

  @canEnumerate(false)
  get y(){
    return this._x
  }
}

let myClass = new SomeClass(1, 2)
for (let key in myClass) {
    console.log(key + " = " + myClass[key]);
}

Output

_x = 1
_y = 2
x = 1

Notice that our decorator function canEnumerate is a decorator factory. This just means our function is a wrapper function for the decorator itself.

The advantage of a wrapper function is we can pass in values to our decorator.

Please note that modifiers will only apply to the first getter/setter defined for a property. If you try to apply an accessor modifier to the corresponding getter/setter for the same property, you will get an error.

Property Decorator

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

class SomeClass {
  @printKey
  public someProp: string;
  constructor(){

  }
}

let myClass = new SomeClass()

Output

someProp

Notice how a property decorator function only takes two arguments:

  • target: The parent class (either the constructor function for a static member or prototype for instance member)
  • propertyKey: The name of the property

Remember the compiler understands our function printKey is a property decorator because of the arguments it takes.

In this example, the @printKey decorator simply logs the key for the property at runtime.

Parameter Decorator

function printIndex(target: any, propertyKey: string, index: number) {
  console.log("Parameter index is: " + index)
}

class SomeClass {
  constructor(){

  }
  public someMethod(first: string, second: string, @printIndex third: string){

  }
}

let myClass = new SomeClass()

Output

Parameter index is: 2

Notice how a parameter decorator function takes three arguments:

  • target: The parent class (either the constructor function for a static member or prototype for instance member)
  • propertyKey: The name of the property
  • propertyKey: The index of the parameter in the argument list

A method parameter's index will be logged at runtime wherever we specify @printIndex

TypeScript Decorators and Reflect Metadata

Property, parameter, and accessor decorators aren't that exciting by themselves.

Unlike class and method decorators, there's not much you can do outside logging property keys at runtime.

To make these more interesting, we can use the reflect-metadata library to add additional runtime functionality:

import "reflect-metadata";

const formatMetadataKey = "format"

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

class Greeter {
    @format("Hello, %s")
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this, "greeting");
        console.log(formatString.replace("%s", this.greeting));
    }
}

let greeter = new Greeter("Alex")
greeter.greet()

This example closely follows the official docs. Notice how we add a @format decorator to the greeting property.

We leverage the reflect-metadata api to store a format string that we use later in our getFormat() function.

The takeaway here is that we are using Reflect to enhance the capabilities of our property decorator. Without Reflect, these types of decorators can do little more than log things...

TypeScript Decorator Use Cases

The examples above demonstrate some of the major typescript decorator use cases. Using decorators, you can add additional validations to fields, create cross-cutting functionality characteristic of AOP.

TypeScript Decorators AOP

Decorators really shine with Aspect Oriented Programming (AOP). AOP is all about addressing cross-cutting concerns with your code.

Cross-cutting concerns include any piece of functionality that can't be easily decomposed into it's own module or results in code duplication or tight coupling.

A popular example is logging. We saw in our examples how decorators can apply consistent logging functionality across different classes or members. Here is a list of popular cross-cutting concerns that can be addressed with decorators.

  • logging
  • caching
  • monitoring
  • business rules
  • data validation

Be sure to check out wikipedia for more information on cross-cutting concerns and how decorators help.

Conclusion

Here are the signatures for the different decorators:

class:

(constructor: Function)

accessor:

(target:any, propertyKey: string, descriptor: PropertyDescriptor)

property:

(target: any, propertyKey: string)

parameter:

(target: Object, propertyKey: string | symbol, parameterIndex: number)

method:

(target: any, propertyKey: string, descriptor: PropertyDescriptor)

Decorators add introspection to code at design time. Using decorators, you can observe and modify classes, methods, properties, parameters, and accessors at runtime.

The reflect-metadata library makes it easier to standardize the metadata you add to objects using decorators.

Decorators shine in addressing cross-cutting concerns characteristic of AOP.

Your thoughts?