First DI: First DI on Interfaces for Typescript Applications

I share one of my libraries called First DI. It has been helping me for many years to solve the problem of dependency injection in browser applications for libraries such as React, Preact, Mithril and others. When writing First DI, the ideology of DI libraries of C # and Java languages, such as autofac, java spring autowired, ninject, and others, was taken as a basis. And just like the libraries from these languages, First DI works based on reflection and Typescript interfaces.

What is DI for?


In short, Dependecy Injection (hereinafter DI) is one of the main parts for building a Clean Architecture. Which allows you to replace any part of the program with another implementation of this part of the program. And DI is a tool for implementing and replacing parts of the program.

In particular, it allows you to:

  • Replace logic implementations individually for each platform. For example, in the browser, show notifications through the Notifications API, and when assembling mobile applications, replace them with notifications through the API of the mobile platform.
  • Replace the implementation of logic in various environments. For example, a payment gateway or a notification distribution system inaccessible from the developer's local machine may be used in the product circuit. Thus, instead of real payments or mailing at the development stage, the logic can be replaced with a stub.
  • Replace data source. For example, instead of a real request to the server, palm off pre-prepared data for testing business logic or layout for compliance with layouts.
  • and many more useful uses, but not about that now ...

To begin, prepare the tools for work.

Preparation for use


To use, you need to do just 3 simple things:

  1. Connect reflect-metadata polyfill .
  2. In the tsconfig file, enable the emitDecoratorMetadata and experimentalDecorators options. The first allows you to generate reflection, the second includes support for decorators
  3. Create any empty decorator, for example const reflection = (..._ params: Object []): void => {} or use the ready-made one from the library.

Now I will demonstrate the use in its simplest form.

Simple use


For example, take a program written using Pure Architecture:

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

@reflection // typescript  
class ProdRepository { //   

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

}

@reflection
class MockRepository { //       

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

}

@reflection
class ProdService {

    constructor(private readonly prodRepository: ProdRepository) { }

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

}

class ProdController { //   React, Preact, Mithril  .

    @autowired() //  
    private readonly prodService!: ProdService;

    // constructor  ,   React, Preact, Mithril
    //      

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

}

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

const controllerInstance = new ProdController(); //   
const data = await controllerInstance.getData();

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

In Pure Architecture you need to add only 2 lines of code, autowired and override in order for DI to start working. And now the program already has an implementation for production and an implementation for testing.

Since First DI was written primarily for client applications, by default dependencies will be implemented as Singleton. In order to globally change this behavior, there are default options, as well as parameters for the autowired decorator and the override () method.

You can change as follows:

//  1 
import { DI, AutowiredLifetimes } from "first-di";
DI.defaultOptions.lifeTime = AutowiredLifetimes.PER_INSTANCE;

//  2  
import { autowired, AutowiredLifetimes } from "first-di";
@autowired({lifeTime: AutowiredLifetimes.PER_INSTANCE})
private readonly prodService!: ProdService;

//  3  
import { override, AutowiredLifetimes } from "first-di";
override(ProdRepository, MockRepository, {lifeTime: AutowiredLifetimes.PER_INSTANCE});

This is the easiest way to use DI. But in the enterprise, instead of specific implementations, interfaces are used, which are a contract for implementations that will be implemented. There is a second mode of operation for this.

Professional use


For professional use, we replace specific implementations with interfaces. But typescript does not generate code for running interfaces in runtime. Fortunately, there is a simple solution, for understanding it is necessary to recall the theory ... what is an interface? This is a completely abstract class!

Fortunately, typescript supports fully abstract classes, generates runtime code, and allows the use of abstract classes to describe types.

We will use this information and write an enterprise program:

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

abstract class AbstractRepository { //    

    abstract getData(): Promise<string>;

}

@reflection
class ProdRepository implements AbstractRepository {

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

}

@reflection
class MockRepository implements AbstractRepository {

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

}

abstract class AbstractService { //    

    abstract getData(): Promise<string>;

}

@reflection
class ProdService implements AbstractService {

    private readonly prodRepository: AbstractRepository;

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

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

}

class ProdController { //   React, Preact, Mithril  .

    @autowired()
    private readonly prodService!: AbstractService;

    // constructor  ,   React, Preact, Mithril
    //      

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

}

override(AbstractService, ProdService);

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

const controllerInstance = new ProdController();
const data = await controllerInstance.getData();

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

Now we have a ready-made Professional program that has no default implementation, and all contracts are replaced by implementations already at the application assembly stage.

It's just that simple to embed DI in your application. And those who wrote in C # and Java will be able to use the existing experience in web development.

Other features


Using multiple copies of DI:

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

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

export class Controller {

    @scopeA.autowired()
    private readonly serviceScopeA!: ProductionService;

    @scopeB.autowired()
    private readonly serviceScopeB!: ProductionService;

    // constructor  ,   React, Preact, Mithril
    //      

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

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

}

Extensibility, you can write your own DI:

import { DI } from "first-di";

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

And many more interesting features are described in the repository on GitHub .

Why is your own written and not used?


The reason is simple - when this DI was written there were no alternatives. This was a time when angular 1 was no longer relevant, and angular 2 was not going to exit. Classes have just appeared in Javascript, and reflection in typescript. Incidentally, the appearance of reflection has become the main impetus for action.

InversifyJS - even in its current form does not suit me. Too much boilerplate. In addition, registering by line or character on the other side kills the possibility of refactoring dependencies.

Liked?


If you liked this DI help make it more popular. Experiment with it, send requests, put stars. GitHub repository .

All Articles