icon-search
jason-leung-330341-unsplash

Typescript decorators

Maciek Fulara 28.11.2016

Decorators are a proposed standard in ECMAScript2016. In Typescript we can enable them by setting the “experimentalDecorators” compiler flag.
Decorators are a mechanism for modifying classes in a declarative fashion.
If you come from a Java background they might look to you just like java annotations. They can be used for all the purposes runtime annotations are used but they are more powerful Annotations are merely a mechanism to store metadata on a type. An annotation on its own does not add new behaviour to its target – to make any effect it needs a processor that can act based on the stored metadata. Decorators on the other hand are functions that take their target as the argument. With decorators we can run arbitrary code around the target execution or even entirely replace the target with a new definition.

There are 4 things we can decorate in ECMAScript2016 (and Typescript): constructors, methods, properties and parameters. We will take a look at each of them in turn by writing a simple logging decorator.

1. Class decorators

A class decorator is a function that accepts a constructor function and returns a contstructor function. Returning undefined is equivalent to returning the constructor function passed in as argument.

const log = <T>(originalConstructor: new(...args: any[]) => T) => {
    function newConstructor(... args) {
        console.log("Arguments: ", args.join(", "));
        new originalConstructor(args);
    }
    newConstructor.prototype = originalConstructor.prototype;
    return newConstructor;
}

@log
class Pet {
    constructor(name: string, age: number) {}
}

new Pet("Azor", 12);
//Arguments: Azor, 12

The log decorator replaces the original constructor with a function that logs the arguments and than invokes the original constructor.

2. Method decorators

A method decorator is a function that accepts 3 arguments: the object on which the method is defined, the key for the property (a string name or symbol) and a property descriptor. The function returns a property descriptor; returning undefined is equivalent to returning the descriptor passed in as argument.

const log = (target: Object, key: string | symbol, descriptor: TypedPropertyDescriptor<Function>) => {
    return {
        value: function( ... args: any[]) {
            console.log("Arguments: ", args.join(", "));
            const result = descriptor.value.apply(target, args);
            console.log("Result: ", result);
            return result;
        }
    }
}

class Calculator {
    @log
    add(x: number, y: number) {
        return x + y;
    }
}

new Calculator().add(1, 3);
//Arguments: 1, 3
//Result: 4

The log decorator replaces the original function with a new function that logs received arguments, executes the original method and stores the result in a local variable, logs and returns the result.

3. Property decorators

Property decorators are similar to method decorators. The only difference is they do not accept property descriptor as argument and do not return anything.

const log = (target: Object, key: string | symbol) => {
    let value = target[key];

    const getter = () =>  {
        console.log("Getting value: ", value);
        return value;
    };
    const setter = (val) => {
        console.log("Setting value: ", val);
        value = val;
    }
    Reflect.deleteProperty[key];
    Reflect.defineProperty(target, key, {
        get: getter,
        set: setter
    });
}

class Box<T> {
    @log
    item: T;
}

const numberInABox = new Box<number>();
numberInABox.item = 12;
numberInABox.item;

//Setting value: 12
//Getting value: 12

The log decorator above redefines the decorated property on the object.

4. Parameter decorators

A parameter decorator is a function that accepts 3 arguments: the object on which the method is defined or the construction function if the decorator is on a constructor argument, the key for the method (a string name or symbol) or undefined in case of constructor argument and the index of the parameter in the argument list. A property decorator does not return anything.

const LOGGED_PARAM_KEY = "logged_param";

//Parameter decorator
const  loggedParam = (target: Object, key: string | symbol, index: number) => {
    const loggedParams: number[] = Reflect.getOwnMetadata(LOGGED_PARAM_KEY, target, key) || [];
    loggedParams.push(index);
    Reflect.defineMetadata(LOGGED_PARAM_KEY, loggedParams, target, key);
}

//Method decorator
const logMethodParams = (target: Object, key: string, descriptor: TypedPropertyDescriptor<Function>) => {
    const loggedParams: number[] = Reflect.getOwnMetadata(LOGGED_PARAM_KEY, target, key) || [];
    return {
        value: function( ... args: any[]) {
            console.log("Logged params: ", loggedParams.map(index => args[index]).join(", "));
            return descriptor.value.apply(target, args);
        }
    }
}

//Class decorator
const logConstructorParams: ClassDecorator = <T>(target: new(...args: any[]) => T) => {
    const loggedParams: number[] = Reflect.getOwnMetadata(LOGGED_PARAM_KEY, target) || [];
    function newConstructor(... args) {
        console.log("Logged params: ", loggedParams.map(index => args[index]).join(", "));
        new target(args);
    }
    newConstructor.prototype = target.prototype;
    return newConstructor;
}

@logConstructorParams
class Box {
    private items = new Array<string>();

    constructor(@loggedParam private initialItem: string) {
        this.items.push(initialItem);
    }   

    @logMethodParams
    addItem(@loggedParam item: string) {
	this.items.push(item);   
    }
}

new Box("first).addItem("second");
//Logged params: first
//Logged params: second

In the code above there is a couple of interesting things going on.
All the decorators from previous examples were replacing their targets with new definitions. This would not work for parameter decorators – we cannot replace parameters with anything – the paramater decorator does not return anything. The only thing we can do is store some additional metadata on the target.
In our example we create an array which we use to store the indices of annotated parameters. We then need to store this array on the target (which is the object on which the method is declared or constructor). A naive approach would be to create a field on the target to store the array.

The loggedParam decorator would look something like the following snippet:


const LOGGED_PARAM_KEY = "logged_param";

const  loggedParam = (target: Object, key: string | symbol, index: number) => {   
    let metaForLoggedParam = target[LOGGED_PARAM_KEY];
    if (!metaForLoggedParam) {
        metaForLoggedParam = {};
        target[LOGGED_PARAM_KEY] = metaForLoggedParam;
    }
	const loggedParams: number[] = metaForLoggedParam[key] || [];    
    loggedParams.push(index);
    target[LOGGED_PARAM_KEY] = loggedParams;
}

The obvious problem with this approach is that it polutes the target with a field that is used only for storing metadata.
We can do better then that by using ES7 Metadata Reflection API. Metadata Reflection API adds additional methods for writing/reading metadata (among others Reflect.defineMetadata and Reflect.getOwnMetadata used in the example) to the Reflect object. This API is still only a proposal, so in order to use it we need to use a polyfill (ie. reflect-metadata).
Reflect.defineMetadata lets us define metadata for the tripplet (metadata_name, target, key) and Reflect.getOwnMetadata lets us read metadata for the same tripplet.

  • metadata_name is any name we choose to associate with the metadata. In the example above it’s the string ‘logged_param’
  • target is the object on which the method is defined in case of method parameters and constructor function in case of constructor paramaters
  • key is the name or symbol of the method for method parameters and is undefined for constructor parameteres

To recoup, what we have so far is a parameter decorator that stores the index of the annotated parameter.
We can now add a method decorator and a constructor decorator that will do the actual logging based on the metadata written by the parameter decorator.
The logConstructorParams/logMethodParams decorators probably look familiar to you. They work just like the constructor/method decorators from previous examples – they replace the constructor/method definition with a new one that executes some custom logic, invokes the original function and returns the result.
In this case the custom logic is reading the indices of parameters to log from the metadata written by loggedParam, finding the parameters to be logged in the arguments array and writing them out to the console.

Logging parameters might look like a trivial example but the combination of parameter decorators for writing metadata and constructor/method decorators for acting based on this metadata is quite powerful.
For a time we had a serious problem with consuming one of our client’s JSON APIs. The response payloads were changing frequently (the field names were different from release to release), mandatory fields were often missing, the system sometimes returned string values where numbers were expected, etc. The tutorial approach for consuming JSON endpoints where you call the http service and cast the response into the expected type obviously didn’t work. Implementing validation / conversion of the response imperatively at all places where we parsed JSON didn’t scale either. What we ended up doing was implementing a bunch of parameter decorators to describe the validation rules and a constructor decorator to run them. If you are interested in the details there will be an article about the library coming shortly.

There are two more things about the decorators you should know – all of the decorator types can be parametrized and all of them compose.

Decorator factories

In order to create a parametrized decorator you create a decorator factory that accepts arguments and returns the decorator function to be used.


const greaterOrEqual = (n: number)  => {
    return (target: Object, key: string | symbol) => {
        let value = target[key];

        const getter = () =>  value;
        const setter = (val) => {
            if (val < n) {
                throw new Error(`Value smaller than ${n}`);
            }
            value = val;
        }
        Reflect.deleteProperty[key];
        Reflect.defineProperty(target, key, {
            get: getter,
            set: setter
        });
    }
}


class Box {
    @greaterOrEqual(0) numberOfItems: number;
}

const box = new Box();
box.numberOfItems = 10; //OK
box.numberOfItems = -1 //throws Error

greaterOrEqual is a decorator factory. Given a number it creates a property decorator that will throw an exception if we try to set the property to a value smaller than the number.
Everytime you see a decorator used with parentheses it’s a decorator factory. Angular2’s Component, Router, NgModule etc. are all decorator factories.

Composing decorators

Decorators compose just like functions.

const printA = (target: Object, key: string | symbol, descriptor: TypedPropertyDescriptor<Function>) => {
    console.log("A");
}

const printB = (target: Object, key: string | symbol, descriptor: TypedPropertyDescriptor<Function>) => {
    console.log("B");
}

class Printer {
    @printA
    @printB
    printC() {
        console.log("C");
    }
}

new Printer().printC();

//output:
//B
//A
//C

We have two method decorators defined (they are almost the same, the only difference is in values they print, so we could have used a decorator factory here). We can see from the output that they are executed from the inside out – just like composed functions would be.

Summary

This was a very brief introduction to decorators. If you would like to see decorators in a real life use case there’s an article about a decorator based JSON validation library coming to our blog soon.

comments: 0