import { View } from 'angular2/angular2'; @View({ template: '<h1>test</h1>' }) class AppComponent { // ... }
function View(settings) { return function(target) { target.template = settings.template; // ... return target; } } @View({ template: '<h1>test</h1>' }) class AppComponent { // ... }
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; })();
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; } }
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; }
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; }
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 }); } }
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); }
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 } }
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; } }
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
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'; } }