Dependency injection in TypeScript applications powered by InversifyJS
Remo H. Jansen
Posted on July 6, 2017
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:
Allow JavaScript developers to write code that adheres to the SOLID principles.
Facilitate and encourage the adherence to the best OOP and IoC practices.
Add as little runtime overhead as possible.
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.
The most important things for us is that the feedback from our users has been very possitive:
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:
Import the required dependencies “reflect-metadata” and “inversify”.
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.
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.
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).
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.
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:
- inversify-binding-decorators
- inversify-inject-decorators
- inversify-express-utils
- inversify-restify-utils
- inversify-vanillajs-helpers
- inversify-tracer
- inversify-logger-middleware
- inversify-devtools (WIP)
- inversify-express-doc
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:
Posted on July 6, 2017
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.