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.
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.
There are three types of dependency injection — constructor injection, method injection, and property injection.
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.
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.
const TYPES = {
Sequelize: Symbol("Sequelize"),
Lodash: Symbol("Lodash"),
SomeClass: Symbol("SomeClass")
};
export { TYPES };
import * as sequelize from "sequelize";
import * as _ from "lodash";
export type Sequelize = typeof sequelize;
export type Lodash = typeof _;
export interface SomeClassInterface {
test(): void;
}
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 };
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 };
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();
{
"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"
}
}
{
"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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With