Click here to Skip to main content
15,879,326 members
Articles / Programming Languages / Typescript

Introducing InversifyJS 2.0: A powerful lightweight IoC container for JavaScript apps powered by TypeScript

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
22 Mar 2016CPOL5 min read 23.5K   10   1
A powerful IoC container for JavaScript apps powered by TypeScript

Introduction

InversifyJS is an open-source inversion of control (IoC) container for TypeScript applications.

About

InversifyJS is a powerful lightweight (4KB) pico inversion of control (IoC) container for TypeScript and JavaScript apps. A pico IoC container uses a class constructor to identify and inject its dependencies. InversifyJS has a friendly API and encourage the usage of the best OOP and IoC practices.

Motivation

JavaScript applications are becoming larger and larger day after day. InversifyJS has been designed to allow JavaScript developers to write code that adheres to the SOLID principles.

Philosophy

InversifyJS has been developed with 3 main goals:

  1. Allow JavaScript developers to write code that adheres to the SOLID principles.

  2. Facilitate and encourage the adherence to the best OOP and IoC practices.

  3. Add as little runtime overhead as possible.

Testimonies

Nate Kohari - Author of Ninject

"Ninject author here. Nice work! I've taken a couple shots at creating DI frameworks for JavaScript and TypeScript, but the lack of RTTI really hinders things. The ES7 metadata gets us part of the way there (as you've discovered). Keep up the great work!"

Background

I released InversifyJS 1.0 about a year ago. Back then it had some nice features like transient and singleton scope but it was far from what I wanted to achieve.

InversifyJS 2.0 (v2.0.0-alpha.3) is finally here and I’m really excited to be able to show you what it is capable of.

InversifyJS 2.0 is lightweight library because one of my main goals is to add as little run-time overhead as possible. The latest build is only 4 KB (gzip) and it has no dependencies.

Another nice thing is that InversifyJS can be used in both web browsers and Node.js. However, in some environments some features may require some ES6 shims (e.g. Providers requires Promises and Proxy requires ES6 Proxies).

Note: This version is a pre-release and is not ready for production use. Some of these APIs are not final and are subject to change.

Let’s take a look to the basic usage and APIs of InversifyJS.

The Basics (TypeScript)

Let’s take a look to the basic usage and APIs of InversifyJS with TypeScript:

Step 1: Declare your interfaces

Our goal is to write code that adheres to the dependency inversion principle. This means that we should "depend upon Abstractions and do not depend upon concretions". Let's start by declaring some interfaces (abstractions).

<code>interface INinja {
    fight(): string;
    sneak(): string;
}

interface IKatana {
    hit(): string;
}

interface IShuriken {
    throw();
}</code>

Step 2: Implement the interfaces and declare dependencies using the @inject decorator

Let's continue by declaring some classes (concretions). The classes are implementations of the interfaces that we just declared.

<code>import { inject } from "inversify";

class Katana implements IKatana {
    public hit() {
        return "cut!";
    }
}

class Shuriken implements IShuriken {
    public throw() {
        return "hit!";
    }
}

@inject("IKatana", "IShuriken")
class Ninja implements INinja {

    private _katana: IKatana;
    private _shuriken: IShuriken;

    public constructor(katana: IKatana, shuriken: IShuriken) {
        this._katana = katana;
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}
</code>

Step 3: Create and configure a Kernel

We recommend to do this in a file named inversify.config.ts. This is the only place in which there is some coupling. In the rest of your application your classes should be free of references to other classes.

<code>import { Kernel } from "inversify";

import { Ninja } from "./entities/ninja";
import { Katana } from "./entities/katana";
import { Shuriken} from "./entities/shuriken";

var kernel = new Kernel();
kernel.bind<INinja>("INinja").to(Ninja);
kernel.bind<IKatana>("IKatana").to(Katana);
kernel.bind<IShuriken>("IShuriken").to(Shuriken);

export default kernel;
</code>

Step 4: Resolve dependencies

You can use the method get<T> from the Kernel class to resolve a dependency. Remember that you should do this only in your composition root to avoid the service locator anti-pattern.

<code>import kernel = from "./inversify.config";

var ninja = kernel.get<INinja>("INinja");

expect(ninja.fight()).eql("cut!"); // true
expect(ninja.sneak()).eql("hit!"); // true
</code>

As we can see the IKatana and IShuriken were successfully resolved and injected into Ninja.

The Basics (JavaScript)

It is recommended to use TypeScript for the best development experience but you can use plain JavaScript if you preffer it. The following code snippet implements the previous example without TypeScript in Node.js v5.71:

<code>var inversify = require("inversify");
require("reflect-metadata");

var TYPES = {
    Ninja: "Ninja",
    Katana: "Katana",
    Shuriken: "Shuriken"
};

class Katana {
    hit() {
        return "cut!";
    }
}

class Shuriken {
    throw() {
        return "hit!";
    }
}

class Ninja {
    constructor(katana, shuriken) {
        this._katana = katana;
        this._shuriken = shuriken;
    }
    fight() { return this._katana.hit(); };
    sneak() { return this._shuriken.throw(); };
}

// Declare injections
inversify.inject(TYPES.Katana, TYPES.Shuriken)(Ninja);

// Declare bindings
var kernel = new inversify.Kernel();
kernel.bind(TYPES.Ninja).to(Ninja);
kernel.bind(TYPES.Katana).to(Katana);
kernel.bind(TYPES.Shuriken).to(Shuriken);

// Resolve dependencies
var ninja = kernel.get(TYPES.Ninja);
return ninja;</code>

Features (v2.0.0 alpha.3)

Let's take a look to the InversifyJS features!

Declaring kernel modules

Kernel modules can help you to manage the complexity of your bindings in very large applications.

<code>let someModule: IKernelModule = (kernel: IKernel) => {
    kernel.bind<INinja>("INinja").to(Ninja);
    kernel.bind<IKatana>("IKatana").to(Katana);
    kernel.bind<IShuriken>("IShuriken").to(Shuriken);
};

let kernel = new Kernel({ modules: [ someModule ] });
</code>

Controlling the scope of the dependencies

InversifyJS uses transient scope by default but you can also use singleton scope:

<code>kernel.bind<IShuriken>("IShuriken").to(Shuriken).inTransientScope(); // Default
kernel.bind<IShuriken>("IShuriken").to(Shuriken).inSingletonScope();
</code>

Injecting a value

Binds an abstraction to a constant value.

<code>kernel.bind<IKatana>("IKatana").toValue(new Katana());
</code>

Injecting a class constructor

Binds an abstraction to a class constructor.

<code>@inject("IKatana", "IShuriken")
class Ninja implements INinja {

    private _katana: IKatana;
    private _shuriken: IShuriken;

    public constructor(Katana: INewable<IKatana>, shuriken: IShuriken) {
        this._katana = new Katana();
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}
</code>
<code>kernel.bind<INewable<IKatana>>("INewable<IKatana>").toConstructor<IKatana>(Katana);
</code>

Injecting a Factory

Binds an abstraction to a user defined Factory.

<code>@inject("IKatana", "IShuriken")
class Ninja implements INinja {

    private _katana: IKatana;
    private _shuriken: IShuriken;

    public constructor(katanaFactory: IFactory<IKatana>, shuriken: IShuriken) {
        this._katana = katanaFactory();
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}
</code>
<code>kernel.bind<IFactory<IKatana>>("IFactory<IKatana>").toFactory<IKatana>((context) => {
    return () => {
        return context.kernel.get<IKatana>("IKatana");
    };
});
</code>

Auto factory

Binds an abstraction to a auto-generated Factory.

<code>@inject("IKatana", "IShuriken")
class Ninja implements INinja {

    private _katana: IKatana;
    private _shuriken: IShuriken;

    public constructor(katanaFactory: IFactory<IKatana>, shuriken: IShuriken) {
        this._katana = katanaFactory();
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}
</code>
<code>kernel.bind<IFactory<IKatana>>("IFactory<IKatana>").toAutoFactory<IKatana>();
</code>

Injecting a Provider (asynchronous Factory)

Binds an abstraction to a Provider. A provider is an asynchronous factory, this is useful when dealing with asynchronous I/O operations.

<code>@inject("IKatana", "IShuriken")
class Ninja implements INinja {

    public katana: IKatana;
    public shuriken: IShuriken;
    public katanaProvider: IProvider<IKatana>;

    public constructor(katanaProvider: IProvider<IKatana>, shuriken: IShuriken) {
        this.katanaProvider = katanaProvider;
        this.katana= null;
        this.shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}

var ninja = kernel.get<INinja>("INinja");

ninja.katanaProvider()
     .then((katana) => { ninja.katana = katana; })
     .catch((e) => { console.log(e); });
</code>
<code>kernel.bind<IProvider<IKatana>>("IProvider<IKatana>").toProvider<IKatana>((context) => {
    return () => {
        return new Promise<IKatana>((resolve) => {
            let katana = context.kernel.get<IKatana>("IKatana");
            resolve(katana);
        });
    };
});
</code>

Injecting a proxy

It is possible to create a proxy of a dependency just before it is injected. This is useful to keep our dependencies agnostic of the implementation of crosscutting concerns like caching or logging.

<code>interface IKatana {
    use: () => void;
}

class Katana implements IKatana {
    public use() {
        console.log("Used Katana!");
    }
}

interface INinja {
    katana: IKatana;
}

@inject("IKatana")
class Ninja implements INinja {
    public katana: IKatana;
    public constructor(katana: IKatana) {
        this.katana = katana;
    }
}
</code>
<code>kernel.bind<INinja>("INinja").to(Ninja);

kernel.bind<IKatana>("IKatana").to(Katana).proxy((katana) => {
    let handler = {
        apply: function(target, thisArgument, argumentsList) {
            console.log(`Starting: ${new Date().getTime()}`);
            let result = target.apply(thisArgument, argumentsList);
            console.log(`Finished: ${new Date().getTime()}`);
            return result;
        }
    };
    katana.use = new Proxy(katana.use, handler);
    return katana;
});
</code>
<code>let ninja = kernelget<INinja>();
ninja.katana.use();
> Starting: 1457895135761
> Used Katana!
> Finished: 1457895135762
</code>

Multi-injection

We can use multi-injection When two or more concretions have been bound to the an abstraction. Notice how an array ofIWeapon is injected into the Ninja class via its constructor:

<code>interface IWeapon {
    name: string;
}

class Katana implements IWeapon {
    public name = "Katana";
}
class Shuriken implements IWeapon {
    public name = "Shuriken";
}

interface INinja {
    katana: IWeapon;
    shuriken: IWeapon;
}

@inject("IWeapon[]")
class Ninja implements INinja {
    public katana: IWeapon;
    public shuriken: IWeapon;
    public constructor(weapons: IWeapon[]) {
        this.katana = weapons[0];
        this.shuriken = weapons[1];
    }
}
</code>

We are binding Katana and Shuriken to IWeapon:

<code>kernel.bind<INinja>("INinja").to(Ninja);
kernel.bind<IWeapon>("IWeapon").to(Katana);
kernel.bind<IWeapon>("IWeapon").to(Shuriken);
</code>

Tagged bindings

We can use tagged bindings to fix AMBIGUOUS_MATCH errors when two or more concretions have been bound to the an abstraction. Notice how the constructor arguments of the Ninja class have been annotated using the @tagged decorator:

<code>interface IWeapon {}
class Katana implements IWeapon { }
class Shuriken implements IWeapon {}

interface INinja {
    katana: IWeapon;
    shuriken: IWeapon;
}

@inject("IWeapon", "IWeapon")
class Ninja implements INinja {
    public katana: IWeapon;
    public shuriken: IWeapon;
    public constructor(
        @tagged("canThrow", false) katana: IWeapon,
        @tagged("canThrow", true) shuriken: IWeapon
    ) {
        this.katana = katana;
        this.shuriken = shuriken;
    }
}
</code>

We are binding Katana and Shuriken to IWeapon but a whenTargetTagged constraint is added to avoid AMBIGUOUS_MATCHerrors:

<code>kernel.bind<INinja>(ninjaId).to(Ninja);
kernel.bind<IWeapon>(weaponId).to(Katana).whenTargetTagged("canThrow", false);
kernel.bind<IWeapon>(weaponId).to(Shuriken).whenTargetTagged("canThrow", true);
</code>

Create your own tag decorators

Creating your own decorators is really simple:

<code>let throwable = tagged("canThrow", true);
let notThrowable = tagged("canThrow", false);

@inject("IWeapon", "IWeapon")
class Ninja implements INinja {
    public katana: IWeapon;
    public shuriken: IWeapon;
    public constructor(
        @notThrowable katana: IWeapon,
        @throwable shuriken: IWeapon
    ) {
        this.katana = katana;
        this.shuriken = shuriken;
    }
}
</code>

Named bindings

We can use named bindings to fix AMBIGUOUS_MATCH errors when two or more concretions have been bound to the an abstraction. Notice how the constructor arguments of the Ninja class have been annotated using the @named decorator:

<code>interface IWeapon {}
class Katana implements IWeapon { }
class Shuriken implements IWeapon {}

interface INinja {
    katana: IWeapon;
    shuriken: IWeapon;
}

@inject("IWeapon", "IWeapon")
class Ninja implements INinja {
    public katana: IWeapon;
    public shuriken: IWeapon;
    public constructor(
        @named("strong")katana: IWeapon,
        @named("weak") shuriken: IWeapon
    ) {
        this.katana = katana;
        this.shuriken = shuriken;
    }
}
</code>

We are binding Katana and Shuriken to IWeapon but a whenTargetNamed constraint is added to avoid AMBIGUOUS_MATCHerrors:

<code>kernel.bind<INinja>("INinja").to(Ninja);
kernel.bind<IWeapon>("IWeapon").to(Katana).whenTargetNamed("strong");
kernel.bind<IWeapon>("IWeapon").to(Shuriken).whenTargetNamed("weak");
</code>

Contextual bindings & @paramNames

The @paramNames decorator is used to access the names of the constructor arguments from a contextual constraint even when the code is compressed. The constructor(katana, shuriken) { ... becomes constructor(a, b) { ... after compression but thanks to @paramNames we can still refer to the design-time names katana and shuriken.

<code>interface IWeapon {}
class Katana implements IWeapon { }
class Shuriken implements IWeapon {}

interface INinja {
    katana: IWeapon;
    shuriken: IWeapon;
}

@inject("IWeapon", "IWeapon")
@paramNames("katana","shuriken")
class Ninja implements INinja {
    public katana: IWeapon;
    public shuriken: IWeapon;
    public constructor(
        katana: IWeapon,
        shuriken: IWeapon
    ) {
        this.katana = katana;
        this.shuriken = shuriken;
    }
}
</code>

We are binding Katana and Shuriken to IWeapon but a custom when constraint is added to avoid AMBIGUOUS_MATCH errors:

<code>kernel.bind<INinja>(ninjaId).to(Ninja);

kernel.bind<IWeapon>("IWeapon").to(Katana).when((request: IRequest) => {
    return request.target.name.equals("katana");
});

kernel.bind<IWeapon>("IWeapon").to(Shuriken).when((request: IRequest) => {
    return request.target.name.equals("shuriken");
});
</code>

The target fields implement the IQueryableString interface to help you to create your custom constraints:

<code>interface IQueryableString {
  startsWith(searchString: string): boolean;
  endsWith(searchString: string): boolean;
  contains(searchString: string): boolean;
  equals(compareString: string): boolean;
  value(): string;
}
</code>

Circular dependencies

InversifyJS is able to identify circular dependencies and will throw an exception to help you to identify the location of the problem if a circular dependency is detected:

<code>Error: Circular dependency found between services: IKatana and INinja</code>

Live demo (ES6)

You can try InversifyJS online at tonicdev.com.

Summary

I want to say thanks the contributors. It’s been a long journey but it has been amazing to work on InversifyJS for the last year. I have many plans for its future and I can’t wait to see what you will create with it.

Please try InversifyJS and let us know what do you think. We want to define a road map based on the real needs of the end users and the only way we can do that is if you let us know what you need and what you don’t like.

Remember that you can visit http://inversify.io/ or the GitHub repo to learn more about InversifyJS.

 

 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Europe Europe
Web development ninja, open-source contributor, writer, speaker, entrepreneur, technology lover and Internet enthusiast. Dublin TypeScript meetup organizer. Web engineer at Aon Centre for Innovation and Analytics (ACIA). Founder of Wolk Software Ltd

Comments and Discussions

 
QuestionExcellent Pin
Member 88105815-Mar-16 11:05
Member 88105815-Mar-16 11:05 
Excellent work Remo, well done!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.