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:
- Allow JS devs to write code that adheres to SOLID principles
- Facilitates and encourage the adherence to the best OOP and IoC practices
- Add as little runtime overhead as possible
- 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")
andSymbol.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
When to use property injection:
- 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
andShuriken
were successfully resolved and injected into Ninja