This article's content
Decorators in TypeScript

Enabling decorators in TypeScript

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

Simple decorator example

function logClassName(target: any) {
    // target is the constructor function of the class
    console.log(`Class name: ${target.name}`);
}

@logClassName
class MyClass {
    // ...
}


const myClassInstance = new MyClass();

As you can see this is a very simple example. Usually we want more, for example

  • we want to pass values to our decorator. We do this by creating a decorator factory (more below)
  • we want to access values from other decorators. We do this by using reflection metadata (more below).

The role of Reflection in decorators

Reflaction is used to temporarily store values. Metadata Reflection API is experimental and not yet part of the ECMAScript standard. This means that its behavior may change in future versions of TypeScript and it may not be supported by all JavaScript environments. To use Reflect.metadata and other functions from the Metadata Reflection API in TypeScript, you need to install the reflect-metadata package and import it at the top of your TypeScript file.

import "reflect-metadata";

@Reflect.metadata("classKey", "classValue")
class MyClass {
    @Reflect.metadata("methodKey", "methodValue")
    myMethod() {
        // ...
    }
}

const classMetadataValue = Reflect.getMetadata("classKey", MyClass);
console.log(classMetadataValue); // "classValue"

const methodMetadataValue = Reflect.getMetadata("methodKey", MyClass.prototype.myMethod);
console.log(methodMetadataValue); // "methodValue"

Class decorator

In the following example the class decorator adds a property to a class.

function myClassDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        propB = "B";
    };
}

@myClassDecorator
class MyClass {
    public propA = "A";
}

const ex = new MyClass();
console.log(ex.propA); // output is 'A'
// @ts-ignore
// TypeScript still gives us error: Property 'decoratorProp' does not exist on type 'Example',
// because the decorator does not change the type 'Example'. So we just ignore it to show, that
// we can call playground.propB successfully anyway.
console.log(ex.propB);

// output is 'B'

Method decorator (using decorator factory)

A decorator factory is a function that returns the decorator function. We use it to allow passing in data (‘num’ in this case). @add is called first, then @subtract and finally myMethod.

// A decorator factory is a function that returns the decorator function.
// We use it to allow passing in data ('num' in this case).
// Must return 'any' to prevent 'Unable to resolve signature of method decorator when called as an expression'
function add(num: number): any {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        // target is the prototype of the class
        // propertyKey is the name of the method
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            console.log(`add ${num}`);
            return originalMethod.apply(this, args) + num;
        }
    };
}

function subtract(num: number): any {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            console.log(`subtract ${num}`);
            return originalMethod.apply(this, args) - num;
        }
    };
}

class MyClass {
    @add(6)
    @subtract(3)
    myMethod(num: number) {
        console.log("myMethod " + num * 2);
        return num * 2;
    }
}

const myInstance = new MyClass();
const result = myInstance.myMethod(5);
console.log(result);

export {}
add 6
subtract 3
myMethod 10
13

Parameter decorator

In the following example we a method decorator @validate and a parameter decorator @required which checks if a property has a value. We use Reflect.defineMetadata to store the index of the parameter that we defined as required in an array. For example, when writing print(@required verbose?: boolean) we mark the parameter at index 0 as required. In the @validate method we use Reflect.getOwnMetadata to read the value and do the actual check.

So this example shows how two decorators work together and how they communicate by using functionality from import "reflect-metadata".

import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

class BugReport {
    type = "report";
    title: string;

    constructor(t: string) {
        this.title = t;
    }

    @validate
    print(@required verbose?: boolean, second?: string, third?: string) {
        if (verbose) {
            return `type: ${this.type}\ntitle: ${this.title}`;
        } else {
            return this.title;
        }
    }
}


function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

function validate(target: Object, propertyName: string, descriptor: TypedPropertyDescriptor<(verbose: boolean) => string>) {
    let method = descriptor.value!;

    descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
        if (requiredParameters) {
            for (let parameterIndex of requiredParameters) {
                if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
                    throw new Error("Missing required argument.");
                }
            }
        }
        return method.apply(this, arguments as any);
    };
}

const report = new BugReport("the title");
console.log(report.print(true, 'B', 'C'));
// type: report
// title: the title

console.log(report.print(false));
// the title

console.log(report.print());
// Error: Missing required argument

Property decorator

In the following example we define @format("Hello, %s") on class property name. The decorator format uses Reflect.metadata which is a default metadata decorator factory. We use it to define metadata, namely the format string "Hello, %s" stored under key Symbol("format"). We can then use Reflect.getMetadata(formatMetadataKey, target, key) to retrieve the metadata value.

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

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

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

class MyClass {
    @format("Hello, %s")
    name: string = "";

    greet() {
        let formatString = getFormat(this, "name");
        return formatString.replace("%s", this.name);
    }
}

const ex = new MyClass()
ex.name = "Mathias";
console.log(ex.greet());

export {};

Accessor decorator

In the following example we use @configurable to define whether an accessor should be configurable or not.

class Point {
    private _x: number;
    private _y: number;

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

    @configurable(false)
    get x() {
        return this._x;
    }

    @configurable(false)
    get y() {
        return this._y;
    }
}

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

const point = new Point(1, 2);
console.log(point.y);

export {}

About Author

Mathias Bothe To my job profile

I am Mathias, born 39 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 15 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.