Decorators in TypeScript

Decorators are a very cool functionality, originally proposed by Google in AtScript (a superset of TypeScript that finally got merged into TypeScript back in early 2015) and also a part of the current standard proposition for ECMAScript 7. In a nutshell, decorators are a way to add metadata to class declarations for use by dependency injection or compilation directives (http://blogs.msdn.com/b/somasegar/archive/2015/03/05/typescript-lt-3-angular.aspx). By creating decorators, we are defining special annotations that may have an impact on the way our classes, methods, or functions behave or just simply altering the data we define in fields or parameters. In that sense, decorators are a powerful way to augment our type's native functionalities without creating subclasses or inheriting from other types.

This is, by far, one of the most interesting features of TypeScript. In fact, it is extensively used in Angular 2 when designing directives and components or managing dependency injection, as we will see from Chapter 4, Enhancing our Components with Pipes and Directives, onwards.

Note

Decorators can be easily recognized by the @ prefix to their name, and they are usually located as standalone statements above the element they decorate, including a method payload or not.

We can define up to four different types of decorators, depending on what element each type is meant to decorate:

  • Class decorators
  • Property decorators
  • Method decorators
  • Parameter decorators

Let's take a look at each of them!

Class decorators

Class decorators allow us to augment a class or perform operations over any of its members, and the decorator statement is executed before the class gets instanced.

Creating a class decorator just requires defining a plain function, whose signature is a pointer to the constructor belonging to the class we want to decorate, typed as Function (or any other type that inherits from Function). The formal declaration defines a ClassDecorator as follows:

declare type ClassDecorator = <TFunction extends Function>(Target: TFunction) => TFunction | void;

Yes, it is really difficult to grasp what this gibberish means, right? Let's put everything in context through a simple example, like this:

function Greeter(target: Function): void {
    target.prototype.greet = function(): void {
        console.log('Hello!');
    }
}

@Greeter
class Greeting {
    constructor() {
        // Implementation goes here...
    }
}

var myGreeting = new Greeting();
myGreeting.greet();   // console will output 'Hello!'

As we can see, we have gained a greet() method that was not originally defined in the Greeting class just by properly decorating it with the Greeter decorator.

Extending the class decorator function signature

Sometimes, we might need to customize the way our decorator operates upon instancing it. No worries! We can design our decorators with custom signatures and then have them returning a function with the same signature we define when designing class decorators with no parameters. As a rule of thumb, decorators taking parameters just require a function whose signature matches the parameters we want to configure. Such a function must return another function, whose signature matches that of the decorator we want to define.

The following piece of code illustrates the same functionality as the previous example, but allows developers to customize the greeting message:

function Greeter(greeting: string) {
    return function(target: Function) {
        target.prototype.greet = function(): void {
            console.log(greeting);
         }
    }
}

@Greeter('Howdy!')
class Greeting {
    constructor() {
        // Implementation goes here...
    }
}

var myGreeting = new Greeting();
myGreeting.greet();    // console will output 'Howdy!'

Property decorators

Property decorators are meant to be applied on class fields and can be easily defined by creating a PropertyDecorator function, whose signature takes two parameters:

  • target: This is the prototype of class we want to decorate
  • key: This is the name of the property we want to decorate

Possible use cases for this specific type of decorator may encompass from logging the value assigned to class fields when instancing objects of such a class and even reacting to data changes on such fields. Let's see an actual example that encompasses both these behaviors:

function LogChanges(target: Object, key: string) {
    var propertyValue: string = this[key];

    if (delete this[key]) {
        Object.defineProperty(target, key, {
          get: function() {
              return propertyValue;
          },
          set: function(newValue) {
              propertyValue = newValue;
              console.log(`${key} is now ${propertyValue}`);
          }
        });
    }
}

class Fruit {
    @LogChanges
    name: string;
}

var fruit = new Fruit();
fruit.name = 'banana';	   // console outputs 'name is now banana'
fruit.name = 'plantain'; // console outputs 'name is now plantain'

The same logic for parametrized class decorators applies here, although the signature of the returned function is slightly different in order to match that of the parameter-less decorator declaration we already saw.

The following example depicts how we can log changes on a given class property and trigger a custom function when this occurs:

function LogChanges(callbackObject: any): Function {
    return function(target: Object, key: string): void {
        var propertyValue: string = this[key];
        if (delete this[key]) {
            Object.defineProperty(target, key, {
              get: function() {
                  return propertyValue;
              },
              set: function(newValue) {
                  propertyValue = newValue;
                  callbackObject.onchange.call(this,        
                                               propertyValue);
              }
            });
        }
    }
}

class Fruit {
    @LogChanges({
        onchange: function(newValue: string): void {
            console.log(`The fruit is ${newValue} now`);
        }
    })
    name: string;
}

var fruit = new Fruit();
fruit.name = 'banana';    // console: 'The fruit is banana now'
fruit.name = 'plantain'; // console: 'The fruit is plantain now'

Method decorators

These special decorators can detect, log, and intervene in how methods are executed. To do so, we just need to define a MethodDecorator function whose payload takes the following parameters:

  • target: This is typed as an object and represents the method being decorated.
  • key: This is a string that gives the actual name of the method being decorated.
  • value: This is a property descriptor of the given method. In fact, it's a hash object containing, among other things, a property named value with a reference to the method itself.

Let's see how we can leverage the MethodDecorator function in an actual example. Suppose we want to build a multipurpose, signature-agnostic logger that will keep track of the output returned by each method in our class upon execution, including some additional data such as the timestamp when the method was executed:

function LogOutput(target: Function, key: string, descriptor: any) {
    var originalMethod = descriptor.value;
    var newMethod = function(...args: any[]): any {
        var result: any = originalMethod.apply(this, args);
        if(!this.loggedOutput) {
            this.loggedOutput = new Array<any>();
        } 
        this.loggedOutput.push({
            method: key,
            parameters: args,
            output: result,
            timestamp: new Date()
        });
        return result;
    };

    descriptor.value = newMethod;
}

As we mentioned earlier, the descriptor parameter contains a reference to the method we want to decorate. With this in mind, nothing prevents us from replacing such a method by our own. We can take advantage of this newly created method to execute the former by passing along to it the same parameters.

Note

Remember that decorator functions are scoped within the class represented in the target parameter, so we can take advantage of that for augmenting the class with our own custom members. Be careful when doing this, since this might override the already existing members though. For the sake of this example, we won't apply any due diligence over this, but handle this with care in your code in the future.

As we can see, our code is pretty simple and straightforward. We are basically storing a reference to the original method in the originalMethod variable, borrowing it from descriptor.value. Then, we are building a new method with a rest argument that ensures we cover any possible method signature out there. Internally, this new method executes the previous one and stores the result alongside some other data (such as the name of the method executed, the parameters used, and the time and date it occurred on) in a newly created class array field, which is created on the spot in case it didn't exist yet. We then return the computed result, if any, as we would expect from the original method we have just overridden.

Let's put our Shiny decorator to the test! Let's create a class with a method performing method computations and logging and see what happens:

class Calculator {
    @LogOutput
    double (num: number): number {
        return num * 2;
    }
}

var calc = new Calculator();
calc.double(11);

console.log(calc.loggedOutput); // Check [Object] array in console

Here, our Shiny Calculator class exposes a method that will double up any number we pick as an input. But, is it properly logging the operations made? Let's inspect in the value of calc.loggedOutput in the browser dev tools console and see what happens.

We can even run the extra mile and add a different method with a completely different type and signature and see whether everything keeps working fine. Add this to the body of the Calculator class:

    @LogOutput
    doNothing (input: any): any {
        return input;
    }

Now, execute the following in the DevTools console command line:

calc.doNothing(['Learning Angular 2', 2016]);

If you check the value of calc.loggedOutput, you will see the new object along with the previous computation we made showing up at the console.

The following line in our example must have caught your attention:

this.loggedOutput = new Array<any>();

We need to properly type the member we aim to create in our class. To do so, and since we cannot annotate the type the normal way here, we leverage the generic constructor of the Array object passing the any type along between the angle brackets.

This chapter will not cover Generics since they are kind of out of the scope of an introductory book like this, but we encourage you to refer to the official site (http://www.typescriptlang.org/Handbook#generics) and delve deeper into the topic. For the time being, you just need to know that generics allow us to enforce a certain type when executing certain custom methods or using specific classes such as arrays. We will see strictly typed arrays or newly created empty objects that are meant to enforce a certain interface thanks to the functionalities provided by the use of generics. Again, please refer to the official site of the TypeScript language for further information.

Parameter decorators

Our last round of decorators will cover the ParameterDecorator function, which taps into parameters located in function signatures. This sort of decorator is not intended to alter the parameter information or the function behavior, but to look into the parameter value and then perform operations elsewhere, such as, for argument's sake, logging or replicating data. The ParameterDecorator function takes the following parameters:

  • target: This is the object prototype where the function, whose parameters are decorated, usually belongs to a class
  • key: This is the name of the function whose signature contains the decorated parameter
  • parameterIndex: This is the index in the parameters array where this decorator has been applied

The following example shows a working example of a parameter decorator:

function Log(target: Function, key: string, parameterIndex: number) {
    var functionLogged = key || target.prototype.constructor.name;
    console.log(`
            The parameter in position ${parameterIndex} at ${functionLogged} has been decorated
     `);
}

class Greeter {
    greeting: string;
    constructor(@Log phrase: string) {
        this.greeting = phrase;
    }
}

// The console will output right after the class above is defined:
// 'The parameter in position 0 at Greeter has been decorated'

You have probably noticed the weird assignation of the functionLogged variable. This is because the value of the target parameter will vary depending on the function whose parameters are being decorated. Therefore, it is different if we decorate a constructor parameter or a method parameter. The former will return a reference to the class prototype and the latter will just return the constructor function. The same applies for the key parameter, which will be undefined when decorating the constructor parameters.

As we mentioned in the beginning of this section, parameter decorators are not meant to modify the value of the parameters decorated or alter the behavior of the methods or constructors where these parameters live. Their purpose is usually to log or prepare the container object for implementing additional layers of abstraction or functionality through higher-level decorators, such as method or class decorators. Usual case scenarios for this encompass logging component behavior or managing dependency injection, as we will see in Chapter 4, Enhancing Our Components with Pipes and Directives.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset