/first-di

Easy dependency injection for typescript applications

Primary LanguageTypeScriptMIT LicenseMIT

First DI

Easy dependency injection for typescript applications

Installation

For the latest stable version:

npm i first-di

Features

  • Easy and powerful dependency injection for any typescript application.
  • 2 modes of work. Optional DI - for most apps. Classic DI - for advanced apps.
  • Support for multiple scopes.
  • Supports multiple life cycles.
  • Dependency Free. Dependency used only for development.

Setup

Install reflect-metadata package and import in root typescript file. This package is needed to support reflection and is a mandatory requirement of Typescript.

In tsconfig.json enable compiler options:

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

Using in Optional DI mode

Just write classes and inject dependencies through class constructors. When the 'resolve' function is called, all dependencies will be resolved.

import { resolve, override, reflection } from "first-di";

@reflection // Typescript will generate reflection metadata
class ProdRepository { // Default implementation

    public async getData (): Promise<string> {
        return Promise.resolve("production");
    }

}

@reflection
class MockRepository implements ProdRepository { // Mock implementation with same interface

    public async getData (): Promise<string> {
        return Promise.resolve("mock");
    }

}

@reflection
class ProdService {

    public constructor (
        private readonly prodRepository: ProdRepository
    ) { }

    public async getData (): Promise<string> {
        return this.prodRepository.getData();
    }

}

@reflection
class ProdStore {

    public constructor (
        // Inject dependency
        private readonly prodService: ProdService
    ) {
        // Other logic here
    }

    public async getData (): Promise<string> {
        return this.prodService.getData();
    }

}

if (process.env.NODE_ENV === "test") { // Override in test environment
    override(ProdRepository, MockRepository);
}

const store = resolve(ProdStore); // Create intance by framework
const data = await store.getData();

if (process.env.NODE_ENV === "test") {
    assert.strictEqual(data, "mock");
} else {
    assert.strictEqual(data, "production");
}

Using in Classic DI mode

In professional mode Interfaces are used instead of implementations. But typescript does not generate Interfaces for working in runtime. But Interface is abstract base class. So instead of Interfaces, you need to write Abstract classes.

import { resolve, override, reflection } from "first-di";

abstract class AbstractRepository { // Abstract instead of interface

    public abstract getData (): Promise<string>;

}

@reflection
class ProdRepository implements AbstractRepository {

    public async getData (): Promise<string> {
        return Promise.resolve("production");
    }

}

@reflection
class MockRepository implements AbstractRepository {

    public async getData (): Promise<string> {
        return Promise.resolve("mock");
    }

}

abstract class AbstractService { // Abstract instead of interface

    public abstract getData (): Promise<string>;

}

@reflection
class ProdService implements AbstractService {

    private readonly prodRepository: AbstractRepository;

    public constructor (prodRepository: AbstractRepository) {
        this.prodRepository = prodRepository;
    }

    public async getData (): Promise<string> {
        return this.prodRepository.getData();
    }

}

@reflection
class ProdStore {

    public constructor (
        private readonly prodService: AbstractService
    ) {}

    public async getData (): Promise<string> {
        return this.prodService.getData();
    }

}

override(AbstractService, ProdService);

if (process.env.NODE_ENV === "test") {
    override(AbstractRepository, MockRepository);
} else {
    override(AbstractRepository, ProdRepository);
}

const store = resolve(ProdStore);
const data = await store.getData();

if (process.env.NODE_ENV === "test") {
    assert.strictEqual(data, "mock");
} else {
    assert.strictEqual(data, "production");
}

Options

First DI has several points for customizing dependency options:

  • Global - DI.defaultOptions: AutowiredOptions. Sets global default behavior.
  • Override - override(fromClass, toClass, options?: AutowiredOptions). Sets behavior overrided dependency.
  • Resolve - resolve(class, options?: AutowiredOptions). Sets behaviors for resolve dependencies.

Options has next properties:

  • lifeTime: AutowiredLifetimes - Sets lifeTime of dependecy.

    SINGLETON - Create one instance for all resolvers.

    PER_INSTANCE - Create one instance for one resolver instance. Also called ‘transient’ or ‘factory’ in other containers.

    PER_OWNED - Create one instance for one type of resolver.

    PER_ACCESS - Create new instance on each access to resolved property.

Scopes

Support multiple scopes

import { DI } from "first-di";
import { ProductionService } from "../services/ProductionService";

const scopeA = new DI();
const scopeB = new DI();

const serviceScopeA = scopeA.resolve(ProductionService);
const dataA = await serviceScopeA.getData();

const serviceScopeB = scopeB.resolve(ProductionService);
const dataB = await serviceScopeB.getData();

API

First DI also has an API for extended use.

  • override - Function. Override dependency and resolve options.
  • resolve - Function. Resolves dependence with default options or specified.
  • singleton - Function. Resolve singleton.
  • instance - Function. Resolve new instance.
  • reset - Function. Reset all singleton list and override list, but don.t reset global options.

Resolve, singleton, instance - can be used to implement the Service Locator.

import { singleton, instance, resolve, AutowiredLifetimes } from "first-di";

class ApiDemo {

    private readonly service3: ApiService3 = resolve(ApiService3, { lifeTime: AutowiredLifetimes.PER_INSTANCE });

    private readonly service4: ApiService4 = singleton(ApiService4);

    private readonly service5: ApiService5 = instance(ApiService5);

}

Extension DI

First DI using OOP and SOLID design principles. Each part of DI can be override or extende after inheritance from base class.

import { DI } from "first-di";

class MyDI extends DI {
    // extended method
    public getAllSingletons(): IterableIterator<object> {
        return this.singletonsList.values();
    }
}