Introducing InversifyJS

A lightweight inversion of control container for TypeScript & JavaScript apps #

Untitled.png

A few months ago I started to think about the possibility of creating a requireJS TypeScript fork in order to add dependency inversion (DI) support.

After some time thinking about it, I decided that mixing a dependency loader and an inversion of control (IoC) container wasn’t the best idea (based on the S, the single-responsibility principle, in SOLID principles). So I started to create a stand-alone IoC container for TypeScript & JavaScript apps.

I started the project thinking about how it should work and how it should be used. During that time I had two major influences:

With those two influences in mind I started to develop InversifyJS. InversifyJS is a lightweight (4KB) inversion of control container for JavaScript apps. It is written in TypeScript, has a really simple API and can be used with multiple frameworks and module loaders. InversifyJS can be used in front-end apps (Web browsers) and back-end apps (NodeJS).

Working with InversifyJS is really easy and only requires 3 simple steps:

1. Declare your entities #

In TypeScript we can define classes and interfaces like the following:

class FooBar implements FooBarInterface {
  public foo : FooInterface;
  public bar : BarInterface;
  public log(){
    console.log("foobar");
  }
  constructor(FooInterface : FooInterface, BarInterface : BarInterface) {
    this.foo = FooInterface;
    this.bar = BarInterface;
  }
}

The class FooBar expects two parameters to be passed as arguments to its constructor. The types of these two arguments are the interfaces FooInterface and BarInteface.

interface FooInterface {
  log() : void;
}

interface BarInterface {
  log() : void;
}

So FooBar has a dependency on two abstractions (interfaces) and not concretions (classes) as the D (the Dependency Inversion principle) in the SOLID principles recommends.

The names of the arguments are relevant here. They need to match the interfaces names because the names are used by the IoC container to identify and inject the dependencies of a class.

Note: In the next release I will add a change so the name of the arguments are no longer relevant and minifiers will be able to rename the function parameters and InversifyJS will still be able to inject the right services.

When compiled into JavaScript the class FooBar looks as follows:

var FooBar = (function () {
    function FooBar(FooInterface, BarInterface) {
        this.foo = FooInterface;
        this.bar = BarInterface;
    }
    FooBar.prototype.log = function () {
        console.log("foobar");
    };
    return FooBar;
})();

The interfaces are not emulated at run-time they are simply removed when we compile our code into JavaScript.

2. Declare your type bindings #

We can then create an instance of the InversifyJS kernel.

var kernel = new inversify.Kernel();

Once we have a kernel instance ready we can define some type bindings. We can set a binding by using the kernel bind method.

A type binding (or just a binding) is a mapping between a service type (an interface), and an implementation type (a class) to be used to satisfy such a service requirement.

kernel.bind(new inversify.TypeBinding<FooInterface>("FooInterface", Foo));
kernel.bind(new inversify.TypeBinding<BarInterface>("BarInterface", Bar));
kernel.bind(new inversify.TypeBinding<FooBarInterface>("FooBarInterface", FooBar));

If we attempt to bind an interface to a class that is not an implementation of that interfaces we will get a compilation error.

// Compilation error: Bar does not implement FooInterface
kernel.bind(new inversify.TypeBinding<FooInterface>("FooInterface", Bar));

In JavaScript we will lose the interfaces (including the nice implementation check at compilation time).

kernel.bind(new inversify.TypeBinding("FooInterface", Foo));
kernel.bind(new inversify.TypeBinding("BarInterface", Bar));
kernel.bind(new inversify.TypeBinding("FooBarInterface", FooBar));

Transient & singleton & transaction scope support #

When we define a type binding we can optionally define the desired scope. The default scope is transient but InversifyJS also supports singleton scope.

kernel.bind(new inversify.TypeBinding<FooInterface>("FooInterface",
  Foo,
  inversify.TypeBindingScopeEnum.Singleton
));

In the next release I will add “transaction” scope. You can learn more about it in the github issue

Contextual binding support #

I decided to not implement transaction scope support but I will implement a much more powerful feature: contextual binding.

Contextual binding will allow us to declare constrained bindings:


// target constrained binding
kernel.bind(new inversify.TypeBinding<IWarrior>(
    "IWarrior",
    Ninja
  ).whenTarget("ChineseArmyTrainer"));

// metadata constrained binding
kernel.bind(new inversify.TypeBinding<IWarrior>(
   "IWarrior",
    Ninja, 
    inversify.TypeBindingScopeEnum.Singleton
  ).whenFlagged("speak", "chinese"));

To achieve this we will use decorators:

@injectable
class Samurai implements IWarrior, ISamurai {
  public fight() {
    console.log("the samurai is fighting!");
  }
}

@injectionTarget
@flagged("speak", "japonese")
class JapaneseArmyTrainer implements ISoldierTrainer {
  public samurai : IWarrior;
  constructor(samurai : IWarrior) {
    this.samurai = samurai;
  }
  public train() {
    for (var i = 0; i < 10; i++) {
      this.samurai.fight();
    }
  }
}

Note: the API above is undergoing analysis and it might change during the implementation process. You can follow the implementation progress here.

3. Resolve dependencies #

You may have noticed that the class FooBar is implementing the FooBarInterface interface.

interface FooBarInterface {
  log() : void;
}

class FooBar implements FooBarInterface {
//...

The FooBarInterface interface is used when we indicate to the IoC container that we want to resolve a dependency on it. We can resolve a dependency by using the kernel resolve method.

var foobar : FooBarInterface = kernel.resolve<FooBarInterface>("FooBarInterface");

The kernel will then create an instance of FooBar (as we indicated in the type binding). Because the FooBar constructor has two dependencies (the interfaces FooInterface and BarInteface) the kernel will resolve them as well and inject them into the FooBar class via its constructor.

Once more, in JavaScript everything is identical but interfaces are removed.

var foobar = kernel.resolve("FooBarInterface");

Composition root vs. service locator #

Our application dependency tree should have one unique root element, known as the application composition root, which is the only place where we should invoke the resolve method.

Invoking resolve every time we need to inject something, as if it was a Service Locator, is an anti-pattern. If we are working with an MVC framework the composition root should be located in the application class, somewhere along the routing logic or in a controller factory class.

For example, in the official marionette integration example, the composition root is the main.ts. This is the only place in the entire application where the resolve method id invoked.

Centralized IoC container configuration #

With InversifyJS we can centralize all the type bindings in one single configuration file. For example, in the official marionette integration example, the Kernel configuration is centralized in the inversify.config.ts file.

Want to learn more? #

Please refer to the integration examples if you need additional help.

If you want learn more about InversifyJS (or contribute) please visit www.inversify.io.

Updates #

 
219
Kudos
 
219
Kudos

Now read this

JSDayIE 2019 tickets now on sale!

We are very happy to announce that after many months of work the JSDayIE tickets are now on sale! About a year ago we started to work on JSDayIE, the first JavaScript conference in Ireland. Ireland has a huge developer community, in... Continue →