Dependency Injection, a deep dive into Typescript Decorators

Lukas Gamper, uSystems GmbH

Webling

Outline

  • Introduction to decorators
  • Decorator metadata
  • Dependency injection

Decorators

  • Annotate / modify classes and class members
  • Syntax: @expression

     

    • where expression evaluates to a function

  • Can be attached to class declarations, methods, accessors, properties or parameters
  • Evaluated at runtime
  • Stage 2 proposal for JavaScript

class C {
    @decorator
    method() {}
}

Configuration

Decorators are an experimental feature that may change in future releases.

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}
tsc --target ES5 --experimentalDecorators

Command Line:

tsconfig.json:

Class Decorators

  • Declared before the class
  • Applied to the constructor of the class
  •  
    • replace declaration with return value, if any
@decorator(constructor)

Class Decorators

function sealed(ctor: Function) {
    Object.seal(ctor);
    Object.seal(ctor.prototype);
}

@sealed
class Greeter {
    private whom: string;
    constructor(whom: string) {
        this.whom = greeting;
    }
    public greet() {
        return "Hello, " + this.whom;
    }
}
var __decorate = ...;
function sealed(ctor) {
    Object.seal(ctor);
    Object.seal(ctor.prototype);
}

var Greeter = (function () {

    function Greeter(whom) {
        this.whom = whom;
    }
    Greeter.prototype.greet 
    = function () {
        return "Hello, " + this.whom;
    };
    Greeter = __decorate([
        sealed
    ], Greeter);
    return Greeter;
}());

function sealed(ctor: Function) {
    Object.seal(ctor);
    Object.seal(ctor.prototype);
}
@sealed
class Greeter {
    private whom: string;
    constructor(whom: string) {
        this.whom = greeting;
    }
    public greet() {

        return "Hello, " + this.whom;
    }




}

Property Descriptor

class Immutable {
    private _value: number;
    constructor(value: number) {
        this._value = value;
    }

    get value() { 
        return this._value; 
    }
    set value(value: number) { 
        this._value = value; 
    }
}
var Immutable = (function () {
    function Immutable(value) {
        this._value = value;
    }
    Object.defineProperty(
        Immutable.prototype, 
        "value", 
        {
            get: function () { 
                return this._value; 
            },
            set: function (value) { 
                this._value = value; 
            },
            enumerable: true,
            configurable: true
        }
    );
    return Immutable;
}());

Method Decorators

  • Declared before the method
  • Applied to the property descriptor of the method
    • replace property descriptor with return value, if any
@decorator(ctorOrProto, methodName, PropertyDescriptor)

Method Decorators

function enumerable(value: boolean) {
    return function (
        target: any,
        propertyKey: string, 
        descriptor: PropertyDescriptor
    ) {
        descriptor.enumerable = value;
    };
}

class Greeter {
    private greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    public greet() {
        return "Hello, " + this.greeting;
    }
}

Accessor Decorators

  • Declared before the accessor declaration (get, set)
  • Applied to property descriptor of accessor declaration
  • Only one decorator before the first accessor specified
  •  
    • replace property descriptor with return value, if any

 

 

 

@decorator(ctorOrProto, accessorName, PropertyDescriptor)

Accessor Decorators

function configurable(value: boolean) {
    return function (
        target: any, 
        propertyKey: string, 
        descriptor: PropertyDescriptor
    ) {
        descriptor.configurable = value;
    };
}

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(true)
    get y() { return this._y; }
}

Property Decorators

  • Declared before the property declaration
  •  
    • no property descriptor provided
    • return value ignored

 

@decorator(ctorOrProto, propertyName)

Parameter Decorators

  • Declared before the parameter declaration
  •  
    • return value ignored 

 

 

 

@decorator(ctorOrProto, parameterName, indexInTheParameterList)

Decorator Factories

  • Function returning the decorator expression at runtime
function enumerable(value: boolean) {
    return function (target: any, key: string, desc: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

class Greeter {
    private greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    public greet() {
        return "Hello, " + this.greeting;
    }
}

Decorator Metadata

  • Design-time type information added, using the @Reflect.metadata
  • Available metadata:
    • Type metadata as "design:type"
    • Parameter type metadata as "design:paramtypes"
    • Return type metadata as "design:returntype".
  • Not yet part of the ECMAScript standard

Configuration

Decorator metadata is an experimental feature that may change in future releases.

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

Command Line:

tsconfig.json:

Decorator Metadata

import "reflect-metadata";
class Point {
    public x: number;
    public y: number;
}
class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}
function validate<T>(target: any, key: string, desc: TypedPropertyDescriptor<T>) {
    let originalSet = desc.set;
    desc.set = function(value: T) {
        let type = Reflect.getMetadata("design:type", target, key);
        if (!(value instanceof type)) {
            throw new TypeError("Invalid type.");
        }
        originalSet(value);
    }
}
var __decorate = ...;
var __metadata = ...;
var Point = ...;
var Line = (function () {
    function Line() {}
    Object.defineProperty(Line.prototype, "p0", {
        get: function () { return this._p0; },
        set: function (value) { this._p0 = value; },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(Line.prototype, "p1", ...);
    __decorate([
        validate,
        __metadata("design:type", Point),
        __metadata("design:paramtypes", [Point])
    ], Line.prototype, "p0", null);
    __decorate([...], Line.prototype, "p1", null);
    return Line;
}());
function validate(target, propertyKey, descriptor) ...

Decorator Metadata

import "reflect-metadata";
class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    @Reflect.metadata("design:type", Point)
    @Reflect.metadata("design:paramtypes", [Point])
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    @Reflect.metadata("design:type", Point)
    @Reflect.metadata("design:paramtypes", [Point])
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

Dependency Injection

  • Injector supplies the dependencies of another objects
  • Improve testability, maintainability, readability
  • Without dependency injection
     
  • With dependency injection
new LoginModel(new UserService(new Transport()))
Injector.resolve<LoginModel>(LoginModel)

Injector

interface Constructor<T> {
    new(...args: any[]): T;
}

const Injector = new class {
    public resolve<T>(target: Constructor<any>): T {
        const tokens = 
            Reflect.getMetadata('design:paramtypes', target) || [];
        const injections = 
            tokens.map(token => Injector.resolve<any>(token));
        return new target(...injections);
    }
};

const injectable = (target: Constructor<object>) => {};

Example

@injectable
class Foo {
    constructor() {
        console.log('foo');
    }
}

@injectable
class Bar {
    constructor(public foo: Foo) {
        console.log('bar');
    }
}

@injectable
class Foobar {
    constructor(public bar: Bar, public foo: Foo) {
        console.log('foobar');
    }
}

const foobar = Injector.resolve<Foobar>(Foobar);
foo
foo
bar
foo
bar
foo
foo
bar
foo
foobar
foo
bar
foo
bar
foo
foobar

Disclaimer

  • Error handling
  • Singleton dependencies
  • Protection of circular dependencies
  • Inject non constructor tokens

Caveats

  • Interfaces are gone after transpilation
    → use classes instead of interfaces
  • Circular dependencies causes trouble
    e.g. Angulars forwardRef
  • Classes without decorators have no metadata

Thanks

Dependency Injection, a deep dive into Typescript Decorators

By gamperl

Dependency Injection, a deep dive into Typescript Decorators

  • 365