HomeAbout

Inversify

Installation

yarn add inversify reflect-metadata

Dependencies

Inversify requires TypeScript >= 4.4 and the experimentalDecorators, emitDecoratorMetadata, types and lib compilation options in your tsconfig.json file

Inversion of Control (IoC)

JS supporting OOP with class-based inheritance can lead to dangerous outcomes (more in oop section).

Inversify tackles this problem by:

  1. Allow JS devs to write code that adheres to SOLID principles
  2. Facilitates and encourage the adherence to the best OOP and IoC practices
  3. Add as little runtime overhead as possible
  4. Provides a good dev experience

Process

Step 1: declare your interfaces and types

Start by creating interfaces (abstractions)

// interfaces.ts export interface Warrior { fight(): string; sneak(): string; } export interface Weapon { hit(): string; } export interface ThrowableWeapon { throw(): string; }

Inversify needs to use the type as identifiers at runtime

Use symbols as identifiers but you can also use classes and/or string literals

You must place type declarations in a separate file

// types.ts const TYPES = { Warrior: Symbol.for("Warrior"), Weapon: Symbol.for("Weapon"), ThrowableWeapon: Symbol.for("ThrowableWeapon") }; export { TYPES };

Step 2: Declare dependencies using the @injectable and @inject decorators

@injectable

Declare concretion (classes)

  • Classes are implementation of the interfaces declared.
  • All the classes are annotated with the @injectable decorator

@inject

When a class has a dependency on an interface we also need to use the @inject decorator to define an identifier for the interface that will be available at runtime

  • In this case we will use the Symbols Symbol.for("Weapon") and Symbol.for("ThrowableWeapon") as runtime identifiers

Why use symbol over string (Id)

In very large applications using strings as the identifiers of the types to be injected by the InversifyJS can lead to naming collisions

  • Symbols enforce that the identifiers are unique

When we use Symbols.for() then we are creating a new symbol if it does not exist or returning an existing symbol if it does; further guaranteeing that the uniqueness is enforced and references point to the same existing

Constructor()

Constructor takes a symbol which Inverisify provides the implementation

  • this allows for swapping of the actual implementation with another swappable dependency such as a mock service for testing

Constructor example

Constructor is a preferred method over plain property injection

  • enforces that all required dependencies are provided at the time of object creation
// file entities.ts import { injectable, inject } from "inversify"; import "reflect-metadata"; import { Weapon, ThrowableWeapon, Warrior } from "./interfaces"; import { TYPES } from "./types"; @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` keyword refers to the instance of the Ninja class // do not use function expression when defining these because it can mess up the scope/reference of `this` keyword this._katana = katana; this._shuriken = shuriken; } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } export { Ninja, Katana, Shuriken };

Property Injection Example

use-case based

  • need more flexibility in setting dependencies after the object is created
@injectable() class Ninja implements Warrior { @inject(TYPES.Weapon) private _katana: Weapon; @inject(TYPES.ThrowableWeapon) private _shuriken: ThrowableWeapon; public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } }

Step 3: Create and configure a container

Create a file named inversify.config.ts

  • This is the only place where there is some coupling

Binding

binding is service being tied to the application container

  • Application container is the runtime idea of dependency injection (Inversify)
// file inversify.config.ts import { Container } from "inversify"; import { TYPES } from "./types"; import { Warrior, Weapon, ThrowableWeapon } from "./interfaces"; import { Ninja, Katana, Shuriken } from "./entities"; 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); export { myContainer };

Step 4: Resolve dependencies

Use get<T> method from the Container class to resolve a dependency

import { myContainer } from "./inversify.config"; import { TYPES } from "./types"; import { Warrior } from "./interfaces"; const ninja = myContainer.get<Warrior>(TYPES.Warrior); expect(ninja.fight()).eql("cut!"); // true expect(ninja.sneak()).eql("hit!"); // true
  • Katana and Shuriken were successfully resolved and injected into Ninja
AboutContact