0.1.4 • Published 2 years ago

@eriicafes/di v0.1.4

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

DI

A simple and lightweight Dependency Injection container for TypeScript 📦

Installation

Install with npm

  npm install @eriicafes/di

or install with yarn

  yarn add @eriicafes/di

Modify your tsconfig.json to include the following settings:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Add a polyfill for the Reflect API (examples below use reflect-metadata)

Install reflect-metadata:

npm install reflect-metadata

Import Reflect polyfill once at the top of the entry file for your project:

// index.ts
import "reflect-metadata";

// your code here...

For other polyfills asides reflect-metadata, you may need to follow guidelines to setup the Reflect polyfill before this library can be used.

Usage/Examples

Dependency Injection is performed on the constructors of decorated classes using Constructor Injection

Decorators

@Injectable()

Class decorator factory that allows classes to be injected to the DIContainer.

import { Injectable, Scope } from "@eriicafes/di";

@Injectable()
class Foo {
  constructor(private bar: Bar) {}
}

@Injectable(Scope.Transient) // control injection scope
class Bar {
  constructor(private baz: Baz) {}
}

@Singleton()

Wraps around @Injectable() to inject class as singleton. By default @Injectable() also injects class as singleton so these two decorators are identical, however using this may be more explicit.

import { Singleton } from "@eriicafes/di";

// singleton instance everywhere
@Singleton()
class Foo {
  constructor(private bar: Bar) {}
}

@LocalSingleton()

Wraps around @Injectable() to inject class as local singleton.

import { LocalSingleton } from "@eriicafes/di";

// singleton instance per container
@LocalSingleton()
class Foo {
  constructor(private bar: Bar) {}
}

@Transient()

Wraps around @Injectable() to inject class as transient.

import { Transient } from "@eriicafes/di";

// new instance everytime
@Transient()
class Foo {
  constructor(private bar: Bar) {}
}

@Inject()

Constructor parameter decorator factory that allows for injecting interfaces, factories or indirect classes to a class.

import { container, Inject, Injectable } from "@erricafes/di";

// interface file
export interface Animal {
  speak(): string;
}

export const Animal = Symbol("Animal");

// concrete implementation
@Injectable()
export class Bird implements Animal {
  public speak() {
    return "humming";
  }
}

// container binding
container.registerTokens([
  {
    identifier: Animal,
    target: Bird,
  },
]);

// some other file (important stuff here)
@Injectable()
class Human {
  // the injection token is the Animal symbol
  constructor(@Inject(Animal) private pet: Animal) {}

  public playWithPet() {
    this.pet.speak();
  }
}

In the example above, the Animal interface has a concrete implementation which is the Bird class, the binding is registered in the container using a javascript Symbol as the token and the Bird class as the target.

NOTE here that both the Animal symbol and the Animal interface have the same name, this does not cause any conflicts in typescript and should be the convention when binding interfaces to their concrete implementations.

Scopes

Scopes determine the lifetime of instances.

  • Singleton (default)

    • each resolve will return the same instance
  • Local Singleton

    • each resolve from the same container will return the same instance
    • resolve from a parent or child container will return a different instance
  • Transient

    • each resolve will return a new instance

Container

The DIContainer stores instances of injectables so they can be resolved later and have their dependencies wired in a clean way. The container recursively resolves classes and their dependencies and caches when applicable. Dependencies in constructor parameters can be automatically resolved as long as they are injectable classes, for interfaces and factories the @Inject() decorator is required.

Injectable classes are classes that fit any of the categories below:

  • have zero constructor parameters, decorator not required (may be classes from external libraries)
  • have only injectable constructor parameters, decorator required

NOTE: classes that have dependencies in their constructors must be decorated as injectable.

Container Instance

A default instance is exported from this package, you may create a new instance by instantiating the DIContainer

import { container, DIContainer } from "@eriicafes/di";

// create child container from exported instance
const childContainer = container.createChildContainer("ChildContainer");

// create new container instance
const newContainer = new DIContainer("NewContainer");

Token Registration

While concrete classes do not require registration, interfaces, factories and indirect classes require an explicit registration to bind tokens to their concrete implementations so they can be used with the @Inject() decorator

factories:

// factory

const FooToken = Symbol("Foo");

// does not have to be decorated with @Injectable()
// could be external class
class Foo {
  constructor(public id: string) {}
}

// container binding
container.registerToken({
  identifier: FooToken,
  factory() {
    return new Foo("id");
  },
});

interfaces:

// interface

interface IBar {}

const BarToken = Symbol("Bar");

@Injectable()
class Bar implements IBar {}

// container binding
container.registerToken({
  identifier: BarToken,
  target: Bar,
});

indirect classes:

// indirect class

const BazToken = Symbol("Baz");

@Injectable()
class Baz {}

// container binding
container.registerToken({
  identifier: BazToken,
  target: Baz,
});

usage:

@Injectable()
class X {
  constructor(
    @Inject(FooToken) public foo: Foo,
    @Inject(BarToken) public bar: IBar,
    @Inject(BazToken) public baz: Baz
  ) {}
}

Resolution

Resolve instances from the container, the container will recursively resolve the class or token and it's dependencies.

// Foo is a class
const foo = container.resolve(Foo);

// IBar is an interface and BarToken is a symbol
const bar = container.resolve<IBar>(BarToken);

Child Containers

Create child containers to create a tree of related containers that may share token registrations along the chain

import { DIContainer } from "@eriicafes/di";

const parentContainer = new DIContainer("ParentContainer");
const container = parentContainer.createChildContainer("Container");
const childContainer = container.createChildContainer("ChildContainer");

Here child containers can resolve tokens registered in their parent containers

Contributing

Contributions are always welcome!

Authors