DEV Community

Cover image for Dependency injection in TypeScript applications powered by InversifyJS
Remo H. Jansen
Remo H. Jansen

Posted on

Dependency injection in TypeScript applications powered by InversifyJS

About

InversifyJS is a lightweight inversion of control (IoC) container for TypeScript and JavaScript apps. InversifyJS uses annotations to identify and inject its dependencies.

The InversifyJS API had been influenced by Ninject and Angular and encourages the usage of the best OOP and IoC practices.

InversifyJS has been developed with 4 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.

  4. Provide a state of the art development experience.

Motivation and background

Now that ECMAScript 2015 version of JavaScript supports classes and that TypeScript brings static types to JavaScript application, the SOLID principles have become more relevant than ever before in the development of JavaScript applications.

InversifyJS was created as a result of the need for tools to enable TypeScript developers to implement an application that adheres to the SOLID principles.

A couple of years ago I was working on some TypeScript applications and I felt that there was a need for an IoC container with great support for TypeScript. At the time there were some IoC containers available for JavaScript applications but none of them were able to provide a developer experience as rich as I was expecting so I decided to try to develop something that would suit my needs.

Adoption

The first commit to the InversifyJS core library took place the 7th of Apr 2015 and the version 1.0.0 was released on npm 10 days later. The version 2.0.0 was released the 11th of Sep 2016, after a year of development. The most recent release (4.2.0 at the time in which this article was published) was published in July 2017.

Since the first release, the project has earned over 1300 stars on GitHub, over 30 contributors and almost 40K monthly downloads on npm.

ddztwqixcaaqrgo

The most important things for us is that the feedback from our users has been very possitive:

selection_011
selection_012
selection_014
selection_016
selection_017
selection_018

Thanks a lot to all our users!

Getting Started

In this tutorial, we are going to showcase how InversifyJS works using Node.js. InversifyJS can be used with JavaScript and TypeScript but it is recommended to use TypeScript for the best developer experience.

To get started you will need Node.js. You can download the Node.js binary for your operating system from the official downloads page.

Once you install Node.js, you will need to install TypeScript. TypeScript can be installed using the npm command which is the default Node.js package manager:

$ npm install -g typescript@2.4.1

If both Node.js and TypeScript has been installed, you should be able to check the installed versions using the following commands.

$ node -v
$ tsc -v

At the time in which this article was published, the latest version of Node.js and TypeScript released were 8.1.0 and 2.4.1 respectively.

At this point, you should be ready to create a new project. We need to create a new folder named “inversify-nodejs-demo” and create a package.json file inside it. We can achieve this by using the npm init command as follows:

$ mkdir inversify-nodejs-demo
$ cd inversify-nodejs-demo
$ npm init --yes

The preceding commands should generate file named “package.json” under the “inversify-nodejs-demo”. We can then install the “inversify” and “reflect-metadata” packages using the Node.js package manager:

$ npm install --save inversify@4.2.0
$ npm install --save reflect-metadata@0.1.10

The “reflect-metadata” module is a polyfill for the reflect meta data API which is required by InversifyJS.

We also need to create a file named “tsconfig.json”. This file contains the configuration for the TypeScript compiler. We can create a “tsconfig.json” file using the following command:

$ tsc -init

You can then copy the following into the generated “tsconfig.json”:

{
  "compilerOptions": {
    "lib": ["es6"],
    "module": "commonjs",
    "target": "es5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

The preceding configuration file contains some compilations required by InversifyJS. At this point, we are ready to write a small demo. Let’s create a new TypeScript file named “index.ts”:

$ touch index.ts

Let’s copy the following TypeScript code into the “index.ts” file:

import "reflect-metadata";
import { interfaces, injectable, inject, Container } from "inversify";

// 1. Declare interfaces
interface Warrior {
  fight(): string;
  sneak(): string;
}

interface Weapon {
  hit(): string;
}

interface ThrowableWeapon {
  throw(): string;
}

// 2. Declare types
const TYPES = {
  Warrior: Symbol("Warrior"),
  Weapon: Symbol("Weapon"),
  ThrowableWeapon: Symbol("ThrowableWeapon")
};

// 3. Declare classes
@injectable()
class Katana implements Weapon {
  public hit() {
    return "cut!";
  }
}

@injectable()
class Shuriken implements ThrowableWeapon {
  public throw() {
    return "hit!";
  }
}

@injectable()
class Ninja implements Warrior {

  private _katana: Weapon;
  private _shuriken: ThrowableWeapon;

  public constructor(
    @inject(TYPES.Weapon) katana: Weapon,
    @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon
  ) {
    this._katana = katana;
    this._shuriken = shuriken;
  }

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

}

// 4. Create instance of Container & declare type bindings
const myContainer = new Container();
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja);
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana);
myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);

// 5. Resolve Warrior type
const ninja = myContainer.get<Warrior>(TYPES.Warrior);

// 6. Check “Katana” and “Shuriken” has been injected into “Ninja”
console.log(ninja.fight()); // "cut!"
console.log(ninja.sneak()); // "hit!"

The preceding file performs the following of tasks:

  1. Import the required dependencies “reflect-metadata” and “inversify”.

  2. Declare some interfaces and some types. Types are unique identifiers used to represent interfaces at runtime. We need these unique identifiers because TypeScript is compiled into JavaScript and JavaScript does not have support for static types like interfaces. We use types to identify which types need to be injected into a class.

  3. Declare some classes that implement the interfaces that we previously declared. These classes will be instantiated by the IoC container and for that reasons they require to be decorated using the “@injectable” decorator. We also need to use the “@inject” decorator to indicate which types need to be injected into a class.

  4. Declare an instance of the “Container” class and then declares some type bindings. A type binding is a dictionary entry that links an abstraction (type) with an implementation (concrete class).

  5. Use the IoC container instance previously declared to resolve the “Warrior” type. We declared a type binding between the “Warrior” type and the “Ninja” class so we can expect the IoC container to return an instance of the “Ninja” class. Because the “Ninja” class has a dependency on the “Weapon” and “ThrowableWapon” types and we declared some bindings for those types we can expect instances of the “Katana” and “Shuriken” classes to be instantiated and injected into the “Ninja” class.

  6. Use the “log” method from the “console” object to check that instances of the Katana” and “Shuriken” has been correctly injected into the “Ninja” instance.

Before running the preceding TypeScript code snippet, we need to compile it into JavaScript. We can use the “tsc” (TypeScript compiler) command and the project option “-p” to use the compilation options that we previously defined in the “tsconfig.json” file:

$ tsc -p tsconfig.json

The preceding command should generate a file named “index.js” under the current directory. We can then run the generated JavaScript file using Node.js

$ node index.js

If everything went well we should see the following text displayed in the console:

cut!
hit!

If we follow the source code we can see how this text comes from methods in the “Katana” and “Shuriken” classes which are invoked through the “Ninja” class. This proves that the “Katana” and “Shuriken” classes have been successfully injected into the “Ninja” class.

InversifyJS in real-world Node.js applications (inversify-express-utils)

What we just saw in the previous section of this article is a basic demo of the core InversifyJS API. When we implement a real world enterprise Node.js application using TypeScript and InversifyJS with Express.js we will end up writing some code that looks as follows:

import * as express from "express";
import { response, requestParams, controller, httpGet, httpPost, httpPut } from "inversify-express-utils";
import { injectable, inject } from "inversify";
import { interfaces } from "./interfaces";
import { Type } from "./types";
import { authorize } from "./middleware";
import { Feature } from "./features";

@injectable()
@controller(
  "/api/user"
  authorize({ feature: Feature.UserManagement })
)
class UserController {

  @inject(Type.UserRepository) private readonly _userRepository: interfaces.UserRepository;
  @inject(Type.Logger) private readonly _logger: interfaces.Logger;

  @httpGet("/")
  public async get(
    @request() req: express.Request,
    @response() res: express.Response
  ) {
    try {
      this._logger.info(`HTTP ${req.method} ${req.url}`);
      return await this._userRepository.readAll();
    } catch (e) {
      this._logger.error(`HTTP ERROR ${req.method} ${req.url}`, e);
      res.status(500).json([]);
    }
  }

  @httpGet("/:email")
  public async getByEmail(
    @requestParams("email") email: string,
    @request() req: express.Request,
    @response() res: express.Response
  ) {
    try {
      this._logger.info(`HTTP ${req.method} ${req.url}`);
      return await this._userRepository.readAll({ where: { email: email } });
    } catch (e) {
      this._logger.error(`HTTP ERROR ${req.method} ${req.url}`, e);
      res.status(500).json([]);
    }
  }
}

As we can see in the preceding code snippet, the inversify-express-utils package allow us to implement routing, dependency injection and even apply some Express.js middleware using a very declarative and developer friendly API. This is the kind of developer experience that we want to enable thanks to InversifyJS and TypeScript.

Features & Tools

The core InversifyJS has a rich API and supports many use cases and features including support for classes, support for Symbols, container API, controlling the scope of the dependencies, injecting a constant or dynamic value, create your own tag decorators, named bindings, circular dependencies

In top of an extensive list of features, we also want to provide developers with a great user experience and we are working on a serie for side-projects to facilitate the integration of InversifyJS with multiple frameworks and to provide developers with a great development experience:

Future development

The main focus of the InverisfyJS project is the core library. We want to continue listening to the needs of the users of the library and keep adding new features to support those use cases. We also want to ensure that we provide users with utilities to facilitate the integration of InversifyJS with whatever framework they are using.

Summary

InversifyJS is a dependency injection library with a rich set of features and a rich ecosystem. If you wish to learn more about InversifyJS please refer to the following links:

Top comments (5)

Collapse
 
theodesp profile image
Theofanis Despoudis • Edited

Great Work Remo,
What are the advantages you think of this library and for example just using a dictionary or a Map of singleton objects that you can pass on to constructors? Something like a Registry Pattern.
Its just I find the decorators in classes to be associated with mixins and I don't find that very appealing

remonsinnema.com/2009/03/01/the-re...

Collapse
 
remojansen profile image
Remo H. Jansen • Edited

Thanks! Good question, InversifyJS allows you to have more control over the life-cycle of dependencies and how the dependency graph is composed than a dictionary of singletons. InversifyJS gives you more power thanks to 3 main features:

Scopes

At the moment the core library allows:

  • Transient scope (one instance each time you call container.get())
  • Singleton scope (same instance for every call to container.get())

I'm working on custom scopes which will allow singletons within a context as opposed to application-level singletons. This will enable things like "Request scope" which can be used to declare singletons at HTTP-request-level.

Contextual constraints

You can inject dependencies based on execution time constraints. InversifyJs supports many types of constraints:

interface BindingWhenSyntax<T> {
    when(constraint: (request: interfaces.Request) => boolean): interfaces.BindingOnSyntax<T>;
    whenTargetNamed(name: string): interfaces.BindingOnSyntax<T>;
    whenTargetTagged(tag: string, value: any): interfaces.BindingOnSyntax<T>;
    whenInjectedInto(parent: (Function|string)): interfaces.BindingOnSyntax<T>;
    whenParentNamed(name: string): interfaces.BindingOnSyntax<T>;
    whenParentTagged(tag: string, value: any): interfaces.BindingOnSyntax<T>;
    whenAnyAncestorIs(ancestor: (Function|string)): interfaces.BindingOnSyntax<T>;
    whenNoAncestorIs(ancestor: (Function|string)): interfaces.BindingOnSyntax<T>;
    whenAnyAncestorNamed(name: string): interfaces.BindingOnSyntax<T>;
    whenAnyAncestorTagged(tag: string, value: any): interfaces.BindingOnSyntax<T>;
    whenNoAncestorNamed(name: string): interfaces.BindingOnSyntax<T>;
    whenNoAncestorTagged(tag: string, value: any): interfaces.BindingOnSyntax<T>;
    whenAnyAncestorMatches(constraint: (request: interfaces.Request) => boolean): interfaces.BindingOnSyntax<T>;
    whenNoAncestorMatches(constraint: (request: interfaces.Request) => boolean): interfaces.BindingOnSyntax<T>;
}
Enter fullscreen mode Exit fullscreen mode

You can do something like:

container.bind<Weapon>("Weapon").to(Katana).whenInjectedInto(Samurai);
container.bind<Weapon>("Weapon").to(Shuriken).whenInjectedInto(Ninja);
Enter fullscreen mode Exit fullscreen mode

Interception

InversifyJS allows you to implement interception thanks to the onActivation handlers and the @postConstruct decorator. You can learn more about IoC and interception here.

Collapse
 
theodesp profile image
Theofanis Despoudis

Great. I suppose this fine level of control will suit more to backend applications or really complex web apps with lots of services when you need something more customizable.

Thread Thread
 
remojansen profile image
Remo H. Jansen

Yes, InversifyJS becomes more valuable as your application grows.

Collapse
 
thohoh profile image
Alex Kozlov • Edited

Thank you for the article. Inversify brough good things in Javascript.

There's also a library called "container-ioc" and is also for Javascript/Typescript & Node.js apps.
It has almost 1 to 1 Angular 4 API and makes use of providers for registration.

I found it more flexible and easier to use in most of my projects.
npmjs.com/package/container-ioc