chart

Decorators & metadata reflection

Learn how decorators make new exciting JavaScript features like reflection or dependency injection possible.

Remo H. Jansen

www.remojansen.com - @RemoHJansen - blog.wolksoftware.com

How all this started?

chart
chart

What are annotations & decorators?

chart


What is the difference between annotations & decorators?

chart

Annotations

We don't know how template is added as metadata to AppComponent:
import { View } from 'angular2/angular2';

@View({
  template: '<h1>test</h1>'
})
class AppComponent {
  // ...
}

Decorators

We can decide how template is added as metadata to AppComponent:
  function View(settings) {
    return function(target) {
      target.template = settings.template;
      // ...
      return target;
    }
  }

  @View({
    template: '<h1>test</h1>'
  })
  class AppComponent {
    // ...
  }

Polyfill

The TypeScript compiler uses a polyfill so we can use decorators today.

// Class without decorators
class AppComponent1 {
  // ...
}

// Class with decorators
@View({
  template: '<h1>test</h1>'
})
class AppComponent2 {
  // ...
}






// Class without decorators
var AppComponent1 = (function () {
  function AppComponent1() {
  }
  return AppComponent1;
})();

// Class with decorators
var AppComponent2 = (function () {
  function AppComponent2() {
  }
  AppComponent2 = __decorate([
    View({
      template: '<h1>test</h1>'
    })
  ], AppComponent2);
  return AppComponent2;
})();

Remember! Decorators are applied to classes NOT instances!

Decorator Types

There are 4 kinds of decorators:

declare type ClassDecorator = (
  target: TFunction
) => TFunction | void;

declare type PropertyDecorator = (
  target: Object,
  propertyKey: string | symbol
) => void;

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

declare type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;
@logClass
class Person {

  @logProperty
  public name: string;
  public surname: string;

  constructor(
    name : string,
    surname : string
  ) {
    this.name = name;
    this.surname = surname;
  }

  @logMethod
  public saySomething(
    @logParameter something : string
  ) : string {

    return this.name + " " +
            this.surname +
            " says: " + something;
  }
}

Class Decorator

A class decorator function is a function that accepts a constructor function as its argument, and returns either undefined, the provided constructor function, or a new constructor function.

function logClass(target: any) {

  // save a reference to the original constructor
  var original = target;

  // a utility function to generate instances of a class
  function construct(constructor, args) {
    var c : any = function () {
      return constructor.apply(this, args);
    }
    c.prototype = constructor.prototype;
    return new c();
  }

  // the new constructor behaviour
  var f : any = function (...args) {
    console.log("New: " + original.name);
    return construct(original, args);
  }

  // copy prototype so intanceof operator still works
  f.prototype = original.prototype;

  // return new constructor (will override original)
  return f;
}

Method Decorator

A method decorator function is a function that accepts three arguments: The object that owns the property, the key for the property (a string or a symbol) and an optional property descriptor.

The function must return either undefined, the provided property descriptor, or a new property descriptor. Returning undefined is equivalent to returning the provided property descriptor.

function logMethod(target, key, descriptor) {

    // save a reference to the original method this way we keep the values currently in the
    // descriptor and don't overwrite what another decorator might have done to the descriptor.
    if(descriptor === undefined) {
      descriptor = Object.getOwnPropertyDescriptor(target, key);
    }
    var originalMethod = descriptor.value;

    //editing the descriptor/value parameter
    descriptor.value = function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i - 0] = arguments[_i];
        }
        var a = args.map(function (a) { return JSON.stringify(a); }).join();
        // note usage of originalMethod here
        var result = originalMethod.apply(this, args);
        var r = JSON.stringify(result);
        console.log("Call: " + key + "(" + a + ") => " + r);
        return result;
    };

    // return edited descriptor as opposed to overwriting the descriptor
    return descriptor;
}

Property Decorator

A property decorator function is a function that accepts two arguments: The object that owns the property and the key for the property (a string or a symbol). The return value of this decorator is ignored.

function logProperty(target: any, key: string) {

  // property value
  var _val = this[key];

  // property getter
  var getter = function () {
    console.log(`Get: ${key} => ${_val}`);
    return _val;
  };

  // property setter
  var setter = function (newVal) {
    console.log(`Set: ${key} => ${newVal}`);
    _val = newVal;
  };

  // Delete property.
  if (delete this[key]) {

    // Create new property with getter and setter
    Object.defineProperty(target, key, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  }
}

Parameter Decorator

A parameter decorator function is a function that accepts three arguments: The function that contains the decorated parameter, the property key of the member (or undefined for a parameter of the constructor), and the ordinal index of the parameter. The return value of this decorator is ignored.

function logParameter(target: any, key : string, index : number) {
  var metadataKey = `__log_${key}_parameters`;
  if (Array.isArray(target[metadataKey])) {
    target[metadataKey].push(index);
  } else {
    target[metadataKey] = [index];
  }
}
Parameter decorator must be used in combination with a method decorator.
class Person {
  // ...
  @logMethod
  public saySomething(@logParameter something : string, somethingElse : string) : string {
    return this.name + " " + this.surname + " says: " + something + " " + somethingElse;
  }
}

It is also recommended to use the reflect-metadata API instead of using class properties.

function logParameter(target: any, key: string, index: number) {
  var metadataKey = `__log_${key}_parameters`;
  var indices = Reflect.getMetadata(metadataKey, target, key) || [];
  indices.push(index);
  Reflect.defineMetadata(metadataKey, indices, target, key);
}

Configurable Decorators

We can allow developers to pass arguments to a decorator when it is consumed:

@logClassWithArgs({ when : { name : "remo"} })
class Person {
  public name: string;

  // ...
}
function logClassWithArgs(filter: Object) {
    return (target: Object) => {
        // implement class decorator here, the class decorator
        // will have access to the decorator arguments (filter)
        // because they are  stored in a closure
    }
}

Decorator Factory

A decorator factory is used to improve the user experience of consuming a decorator.

function log(...args : any[]) {
  switch(args.length) {
    case 1:
      return logClass.apply(this, args);
    case 2:
      return logProperty.apply(this, args);
    case 3:
      if(typeof args[2] === "number") {
        return logParameter.apply(this, args);
      }
      return logMethod.apply(this, args);
    default:
      throw new Error();
  }
}










@log
class Person {

  @log
  public name: string;
  public surname: string;

  constructor(
    name : string,
    surname : string
  ) {
    this.name = name;
    this.surname = surname;
  }

  @log
  public saySomething(
    @log something : string
  ) : string {

    return this.name + " " +
            this.surname +
            " says: " + something;
  }
}

The reflect-metadata API

Is an external static dictionary to read and write metadata. We need to use the --emitDecoratorMetadata compiler options and the reflect-metadata npm package.

function logParamTypes(target : any, key : string) {
  var types = Reflect.getMetadata("design:paramtypes", target, key);
  var s = types.map(a => a.name).join();
  console.log(`${key} param types: ${s}`);
}

class Foo {}
interface IFoo {}

class Demo{
  @logParameters
  doSomething(
    param1 : string,
    param2 : number,
    param3 : Foo,
    param4 : { test : string },
    param5 : IFoo,
    param6 : Function,
    param7 : (a : number) => void,
  ) : number {
      return 1
  }
}

// doSomething param types: String, Number, Foo, Object, Object, Function, Function

Real-world applications

Inversion of control (IoC) containers

import { Inject } from 'angular2/angular2';

@Inject(Engine, Tires, Doors)
class Car {
  constructor(
    engine,
    tires,
    doors
  ) {
    ...
  }
}

Many other applications

import { Component } from 'angular2/angular2';

@Component({
  selector: 'app',
  template: '<h1>test</h1>'
})
class App {
  constructor() {
    this.name = 'World';
  }
}

And the winner is...


Let's find out who will win a free physical copy of my first book!


Thanks!

Do you have any questions?

Remo H. Jansen

www.remojansen.com - @RemoHJansen - blog.wolksoftware.com