Basic Typescript Dependency Injection with Decorators

Posted on Sat 01 August 2015 in TypeScript

Typescript 1.5 introduced decorators to the language which lets us experiment with meta-programming. Metadata driven Dependency Injection frameworks allow you to write highly decoupled units which are easy to test and switch out between projects / frameworks. Let's see how we can use decorators to get some simple property injection working with TypeScript.

Let's start with an example of what we want to achieve:

class LoginService {
  userModel : UserModel;
  performLogin() : void {
    if (!this.userModel.isLoggedIn()) {
      // ... implementation omitted...
    }
  }

Here our LoginService wants to check if our user is logged in before doing anything; as a result we need to provide a reference to the UserModel before invoking LoginService#performLogin(), we can do this manually:

const myUserModel = new UserModel();
const myLoginService = new LoginService();

// Manually inject the dependency before using the Service.
myLoginService.userModel = myUserModel;
myLoginService.performLogin();

We can use a decorator to record the fact that LoginService has a dependency on UserModel:

class LoginService {
  @inject('UserModel')
  userModel : UserModel

Now we need to implement our @inject decorator, the contract requires us to export a factory function (ie: a function which returns a function) - this factory function will be invoked each time a new instance of the supplied Class is constructed giving us a chance to modify the behavior of the program at run-time.

function inject(injectionKey : string) {

    // Our decorator provides a factory function which will be invoked with an
    // instance of the decorated Class and the name of the decorated property.
    return function recordInjection(target : Object, decoratedPropertyName : string) : void {

        // Get a reference to the Class of the target object which has been
        // decorated.
        const targetType : { __inject__?: Object } = target.constructor;

        if (!targetType.hasOwnProperty('__inject__')) {
            targetType.__inject__ = {};
        }

        // Associate this property with the injectionKey provided in the 
        // decorator call
        targetType.__inject__[decoratedPropertyName] = injectionKey;
    };
}

Now we need somewhere to record injection mappings so we have values to inject into decorated properties:

class Injector {
    private valuesByInjectionKey : { [ injectionKey : string ] : any } = {};

    /**
     * Associate an injectionKey with a value so that the supplied value can be 
     * injected into properties of the target Class decorated with the `@inject` 
     * decorator.
     *
     * @param {string} injectionKey
     * @param {*} value
     */
    mapValue(injectionKey : string, value : any) : void {
        this.valuesByInjectionKey[injectionKey] = value;
    }
}

Continuing our original example we would map the injectionKey UserModel to the instance of UserModel that we want injected, eg:

const injector = new Injector();
injector.mapValue('UserModel', new UserModel());

Finally we need to introduce a factory function which will instantiate a class but also fulfill injections based on mappings in the Injector - to enable this we add an #instantiate() method to Injector.

class Injector {
    /**
     * Create a new instance of the supplied Class fulfilling any property 
     * injections which are present in the injectionRules map.
     */
    instantiate<T>(Class : { new(...args: any[]) : T }) : T {
        // Start by creating a new instance of the target Class.
        const instance : any = new Class();

        // Loop through all properties decorated with `@inject()` in this Class and
        // try to satisfy them if there is a mapped value.
        for (let injectionPoint of this.getInjectionPoints(Class)) {
            const injectionValue : any = this.valuesByInjectionKey[injectionPoint.injectionKey];

            // Perform the injection if we have a value assigned to this injectionKey.
            if (injectionValue) {
                instance[injectionPoint.propertyName] = injectionValue;
            }
        }

        return instance;
    }

    private getInjectionPoints<T>(Class : { __inject__?: { [ prop : string ] : string } }) : Array<InjectionPoint> {
        var result : Array<InjectionPoint> = [];

        // Retrieve the `__inject__` hash created by the @inject decorator from the
        // target Class.
        if (Class.hasOwnProperty('__inject__')) {
            result = Object.keys(Class.__inject__)
                .map((propertyName : string) => {
                    return {
                        propertyName: propertyName,
                        injectionKey: Class.__inject__[propertyName]
                    }
                });
        }

        return result;
    }
}

interface InjectionPoint {
    propertyName : string;
    injectionKey : string;
}

The Injector#intantiate() method looks for the #__inject__ property added to a Class' constructor function by the @inject decorator and then uses the values that hash contains to fulfil the decorated dependencies of the target Class - ie:

const myUserModel = new UserModel();
injector.mapValue('UserModel', myUserModel);

// `#userModel` will be injected automatically by by injector
var myLoginService = injector.instantiate(LoginService)
myLoginService.performLogin();

A complete example with Mocha Tests provided over at github.com/jonnyreeves/ts-prop-injection.

Part 2: Adding support of method injection.