Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency injection: recommended pattern for injecting NPM modules

I'd like to use Inversify to remove hard dependencies on NPM modules and inject them as constructor arguments instead. This seemed like it would be a lot simpler before I gave it a shot.

It turns out that most DefinitelyTyped modules don't bother to export interfaces, and when they do, they rarely include an interface that represents the entire module. Further, when a class is exported, I still have to manually define a constructor interface for that class.

This means I have to do something like this for pretty much every module:

import * as ConcreteModule from 'module'
import { ContainerModule } from 'inversify'

export interface ModuleInstance {
  // copy a ton of stuff from DefinitelyTyped repo,
  // because they didn't export any interfaces
}

export interface ModuleConstructor {
  new (...args: any[]): ModuleInstance
}

export const ModuleConstructorSymbol = Symbol('ModuleConstructor')

export const ModuleContainer = new ContainerModule((bind) => {
  bind<ModuleConstructor>(ModuleConstructorSymbol).toConstantValue(ConcreteModule)
})

Is there any way to simplify some of this? There's just so much overhead to inject an NPM module, and no guidance whatsoever from the Inversify docs. Managing the names for all the different imports/exports you need (interface, symbol, and container) is a pain and requires coming up with some kind of consistent naming scheme. It seems like without TypeScript supporting some way to automatically create an interface from a module, there's just no way to inject NPM packages in a sane manner.

I suppose I could just use jest's automock feature for modules, but I really don't like designing my code in such a way that it is only unit testable with a specific testing framework.

It seems like this could be at least a bit more achievable if I could just do this:

import * as ConcreteModule from 'module'

export interface TheModule extends ConcreteModule {}

But this only works if the module exports a class (not a factory) and still doesn't really help me with the constructor.

like image 625
sbking Avatar asked Apr 27 '17 23:04

sbking


People also ask

Should I use dependency injection NodeJS?

Most would say that dependency injection in NodeJS is unnecessary because you can import dependencies to a module using the command “require”. Yet, such an approach creates a more complex code to maintain and test.

How many ways can you inject dependency?

There are three types of dependency injection — constructor injection, method injection, and property injection.

How does dependency injection work in node JS?

What is a dependency injection? DI is a pattern where, instead of creating or requiring dependencies directly inside a module, we pass them as parameters or reference. At first glance, it might be quite difficult to understand.


1 Answers

The following example demonstrates how to inject npm modules (lodash & sequelize) into a class SomeClass.

The directory structure looks as follows:

src/
├── entities
│   └── some_class.ts
├── index.ts
└── ioc
    ├── interfaces.ts
    ├── ioc.ts
    └── types.

/src/ioc/types.ts

const TYPES = {
    Sequelize: Symbol("Sequelize"),
    Lodash: Symbol("Lodash"),
    SomeClass: Symbol("SomeClass")
};

export { TYPES };

/src/ioc/interfaces.ts

import * as sequelize from "sequelize";
import * as _ from "lodash";

export type Sequelize = typeof sequelize;
export type Lodash = typeof _;

export interface SomeClassInterface {
    test(): void;
}

/src/ioc/ioc.ts

import { Container, ContainerModule } from "inversify";
import * as sequelize from "sequelize";
import * as _ from "lodash";
import { TYPES } from "./types";
import { Sequelize, Lodash } from "./interfaces";
import { SomeClass } from "../entities/some_class";

const thirdPartyDependencies = new ContainerModule((bind) => {
    bind<Sequelize>(TYPES.Sequelize).toConstantValue(sequelize);
    bind<Lodash>(TYPES.Lodash).toConstantValue(_);
    // ..
});

const applicationDependencies = new ContainerModule((bind) => {
    bind<SomeClass>(TYPES.SomeClass).to(SomeClass);
    // ..
});

const container = new Container();

container.load(thirdPartyDependencies, applicationDependencies);

export { container };

/src/entitites/some_class.ts

import { Container, injectable, inject } from "inversify";
import { TYPES } from "../ioc/types";
import { Lodash, Sequelize, SomeClassInterface } from "../ioc/interfaces";

@injectable()
class SomeClass implements SomeClassInterface {

    private _lodash: Lodash;
    private _sequelize: Sequelize;

    public constructor(
        @inject(TYPES.Lodash) lodash,
        @inject(TYPES.Sequelize) sequelize,
    ) {
        this._sequelize = sequelize;
        this._lodash = lodash;
    }

    public test() {
        const sequelizeWasInjected = typeof this._sequelize.BIGINT === "function";
        const lodashWasInjected = this._lodash.cloneDeep === "function";
        console.log(sequelizeWasInjected); // true
        console.log(lodashWasInjected); // true
    }

}

export { SomeClass };

/src/index.ts

import "reflect-metadata";
import { container } from "./ioc/ioc";
import { SomeClassInterface } from "./ioc/interfaces";
import { TYPES } from "./ioc/types";

const someClassInstance = container.get<SomeClassInterface>(TYPES.SomeClass);
someClassInstance.test();

/package.json

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "inversify": "^4.1.0",
    "lodash": "^4.17.4",
    "reflect-metadata": "^0.1.10",
    "sequelize": "^3.30.4"
  },
  "devDependencies": {
    "@types/lodash": "^4.14.63",
    "@types/sequelize": "^4.0.51"
  }
}

/tsconfig.json

{
    "compilerOptions": {
        "target": "es5",
        "lib": ["es6", "dom"],
        "types": ["reflect-metadata"],
        "module": "commonjs",
        "moduleResolution": "node",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

This example is now available in the inversify docs.

like image 191
Remo H. Jansen Avatar answered Oct 30 '22 08:10

Remo H. Jansen