Typescript decorators with examples

Posted on Feb 24, 2022
Note: This article was written a while ago and may contain outdated information. Please verify the details before relying on it. If I express opinions or recommendations, they might not reflect my current views. For this reason, I recommend checking for more recent articles on the same topic.

Decorators in Typescript help us to add annotations and a meta-programming syntax for class declarations and member.

You can add decorators to class declarations, method declarations, property declarations and parameters. Frameworks like NestJS and Angular are using decorators.

Let’s have a short look at decorators and how to use them.

*Currently decorators are an experimental feature. So if any of this code samples don’t work, things may have changed.

Project setup

If you want to try the following examples, start with initializing a new Typescript project in a directory:

tsc --init

As decorators are experimental, you need to enable this feature manually. In the compilerOptions of the tsconfig.json we need to uncomment or add the following line:

"experimentalDecorators": true

Create a Calculator class and store it in a file:

export class Calculator {
    public increment(toIncrement: number) {
        return toIncrement + 1;
    }
}

Create an index.ts with the following code:

import { Calculator } from "./calculator.class";

const calculator = new Calculator();

Then create a decorators.ts file, in which you’ll store your decorators.

Now you should be able to call calculator.increment(1).

Defining decorators

A decorator is a function which can be a simple function or a decorator factory, which set ups a decorator.

If you create a decorator, then you need to implement the specific type corresponding to the place where you want to add the decorator:

Decoration of… Type signature
Class (Constructor: {new(...any[]) => any}) => any
Method (classPrototype: {}, methodName: string, descriptor: PropertyDescriptor) => any
Static method (Constructor: {new(...any[]) => any}, methodName: string, descriptor: PropertyDescriptor) => any
Method parameter (classPrototype: {}, paramName: string, index: number) => void
Static method parameter (Constructor: {new(...any[]) => any}, paramName: string, index: number) => void
Property (classPrototype: {}, propertyName: string) => any
Static property (Constructor: {new(...any[]) => any}, propertyName: string) => any
Property getter/setter (classPrototype: {}, propertyName: string, descriptor: PropertyDescriptor) => any
Static property getter/ setter (Constructor: {new(...any[]) => any}, propertyName: string, descriptor: PropertyDescriptor) => any

As you see in the table the expected type signature for a method is the following:

type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

What does this mean?

You need to create a method which accepts the following params:

target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>`

Furthermore it needs to return TypedPropertyDescriptor<T> | void.

Simple function

A simple decorator can look like follow:

export function y(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("applied to", propertyKey)
}

You can apply it by annotating the method with @y.

@y
public increment(toIncrement: number) {
    return toIncrement + 1;
}

Here are two things which are interesting to know: First, you don’t need to use parenthesis. Even if you don’t call the increment() method you should see the following in the logs:

applied to increment

-> Decorators evaluate only one time even if you don’t use them. -> Furthermore you don’t need to use parenthesis if you’re don’t using decorator factories.

Decorator factory

A decorator factory is a function which returns a function with the implemented decorator type.

This has the benefit that you can add parameters to your decorator.

See the following example:

export function x(apply: boolean) {
  return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    if (apply) {
      console.log("decorator applied to", propertyKey)
    }
  }
}

If you annotate your method with x(true) you should see the following log:

applied to increment

Respectively if you annotate it with x(false) you’ll see no logs! But the decorator got evaluated anyway.

You can also see that you cannot just annotate with x like in the example before.

Method Decorator which applies an action on method call

Well, I saw this examples and asked myself how to implement a decorator which fires an action on a method call.

As an example think of a call observer which prints the input parameters and output parameters.

To make this possible we can make use of the PropertyDescriptor interface which we get in every decorator.

This has the value value which you can overwrite with your own method.

See the following example:

export function observe(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any) {
    console.log('Params: ', ...args);
    const result = originalMethod.call(this, ...args);
    console.log('Result: ', result);
    return result;
  }
}

If apply your method with @observe and call the increment method above, then you should see the following in your console (depending on your arguments):

Params:  0
Result:  1

Limitations

You are able to apply decorators on classes and extend by new fields.

But this will not change the type itself, so that you’ll get an error by the linter and compiler, like in this example.

Further reading