• +91 9790 650 659
  • This email address is being protected from spambots. You need JavaScript enabled to view it.
  • One of TypeScripts most awesome features are, in my opinion, decorators. Decorators are a mechanism for modifying classes in a declarative fashion. Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members.

    The decorator pattern modifies instances of existing objects without affecting the root object or siblings. Typically the pattern extends a base interface by toggling features, setting attributes, or defining roles. Instances of the object being decorated should usually be able to interact, but they don't have to have identical interfaces.

    TypeScript provides experimental decorator support. The ECMAScript decorator proposal has reached stage 2, so we could see them in vanilla JS eventually. TypeScript provides class, method, parameter, and property decorators. Each can be used to observe the decorated objects (mentioned heavily in the docs). All but the parameter decorator can be used to modify the root object.

    If you come from a Java background they might look to you just like java annotations and if you are from c# background they might look to you like attributes. 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.

    Let’s take a look at what decorators are, and how they can be used to make your code cleaner and more easily understandable.

    There are 4 things we can decorate in ECMAScript2016 and Typescript:

    • Constructors
    • Methods
    • Properties
    • 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 logClass = <T>(originalConstructor: new(...args: any[]) => T): any => {

         const newConstructor = (...args=> {
             console.log(`Arguments: ${args.join(', ')}`);
             new originalConstructor(args);

        }

        newConstructor.prototype = originalConstructor.prototype;
        return newConstructor;
    }

    @logClass
    class User {
        constructor(name: stringemail: stringage: number) {

        }
    }

    @logClass
    class Product {
        constructor(name: stringprice: string) {

        }
    }

    new User('Ranjithprabhu'This email address is being protected from spambots. You need JavaScript enabled to view it.'27);
    new Product('Pen''$2.00 USD');

    In the above example, the logClass decorator replaces the original constructor with a function that logs the arguments and than invokes the original constructor. And we can use the decorator for how many classed ever we want as mentioned in the above example.

    The above example logs the following in the console.

    Arguments: Ranjithprabhu, This email address is being protected from spambots. You need JavaScript enabled to view it., 27
    Arguments: Pen, $2.00 USD

    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.

    constlogMethod= (target:Object,targetKey:string,descriptor:TypedPropertyDescriptor<any>):any=> {
        return {
           value: (...args:any[]):any=> {
                console.info(`'${targetKey}' method is executed`);
                console.log(`Arguments:${args.join(', ')}`);

                constresult=descriptor.value.apply(target,args);
                if (result) {
                     console.log(`Result:${result}`);
                }

            returnresult;
         }
       }
    }

    classCalculator {

           @logMethod
           add(x:number,y:number):number {
              returnx+y;
           }

           @logMethod
           sub(x:number,y:number):number {
               returnx-y;
           }

           @logMethod
           print(x:number,y:number):void {
                console.log('Value of X:',x);
                console.log('Value of Y:',y);
           }

    }

    newCalculator().add(1,3);
    newCalculator().sub(5,3);
    newCalculator().print(10,8);

    In the above example, the logMethod decorator replaces the original function with a new function that logs the method name to be executed, received arguments, executes the original method and stores the result in a local variable. Again logs and returns the result. If the method is not going to return anything (similar to print method), then the result will be undefined.

    The above example logs the following in the console.

     'add' method is executed
    Arguments: 1, 3
    Result: 4
    'sub' method is executed
    Arguments: 5, 3
    Result: 2
    'print' method is executed
    Arguments: 10, 8
    Value of X: 10
    Value of Y: 8

    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.

    constlogProperty= (target:Object,key:string|symbol):any=> {
        letvalue=target[key];
        constgetter= ()=> {
            console.log(`Getting value:${value}`);
            returnvalue;
        };

        constsetter= (val)=> {
            console.log(`Setting value:${val}`);
            value=val;
        };

        Reflect.deleteProperty[key];
        Reflect.defineProperty(target,key, {
            get:getter,
            set:setter
        });
    }

    classItem<T> {
        @logProperty
        quantity:T;
    }

    constpen=newItem<number>();
    pen.quantity=12;
    pen.quantity;

    The logProperty decorator above redefines the decorated property on the object. In the above example, we are redefining the get and set property.

    The above example logs the following in the console as we are logging the values on set and get.

     Setting value: 12
    Getting value: 12

    4. Parameter decorators

    Parameter decorators are triggered when the parameter is declared, but they don't affect anything. We can't observe the parameter's value, because that's attached long after the parameter is decorated. We can't change the state, because that's also not created until long after the parameter is decorated. All we can do is define metadata and that's about it.

    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.

    constLOGGED_PARAM_KEY='logged_param';

    constlogParam= (target:Object,key:string|symbol,index:number)=> {
         constloggedParams:number[]=Reflect.getOwnMetadata(LOGGED_PARAM_KEY,target,key)|| [];
         loggedParams.push(index);
         Reflect.defineMetadata(LOGGED_PARAM_KEY,loggedParams,target,key);
    }

    classInventory {
        privateitems=newArray<string>();

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

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

    }

    newInventory('Pen').addItem('Pencil');

    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 created an array which we use to store the indices of decorated 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.

    There are two more things about the decorators you should know

    • all of the decorator types can be parametrized
    • all of them compose.

    Decorator factories

    Decorators have well-defined signatures without room for extension. To pass new information into the decorators, we can use the factory pattern. A factory provides a uniform creation interface whose details are delegated to and managed by children.

    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.

    While this example was fairly simple, decorator factories are capable of much more. Anything you pass to the factory can be used to assemble the decorator. As the decorator's return is used by everything except for parameter decorators, you can customize the instance using anything in the scope. Decorators aren't limited to building up; they can also tear down.

    Angular2’s Component, Router, NgModule etc. are all decorator factories.

    Composing decorators

    Function composition is a very useful tool. It requires two functions, f: A → B and g: C → D, with some conditions on their domains and ranges. To compose f with g, i.e. f(g(x)), D must be a subset of A, i.e. the input of f must contain the output of g.

    This is much simpler in code. For the most part, we can compose f with g when g's return value is identical to f's input (completely ignoring containment because that gets messy). As we've seen, decorators seem to return a single object while they consume an array of arguments. That would suggest they cannot be composed. However, decorators aren't actually being called and run on the stack by themselves. TypeScript surrounds the decorator calls with several other things behind the scenes, which, rather magically, means decorators can be composed with other decorators.

    Decorators compose just like functions. 

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

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

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

    new Printer().printC(); 

    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.

    The above example logs the following in the console.

    B printed
    A printed
    C printed

    Summary

    Decorators provide a very good way of wrapping code inside a class in a very similar way to how you can already do so for freestanding functions. This provides a good way of writing some simple clean code that can be applied to a lot of places in a very clean and easy-to-understand manner.

    You can see the reference code implemented in the Github in the following URL: https://github.com/ranjithprabhuk/Typescript-Decorators

    Thanks for Reading!!

Search

Subscribe