There's no syntax in JavaScript that supports such mapping. Even if custom function signature parser were written to to provide desired behaviour for destructured params like function({lodash:_}) ...
, it would fail for transpiled functions, which is a major flaw. The most straightforward way to handle this is
function foo(lodash){
const _ = lodash;
...
}
And it obviously won't work for invalid variable names like lodash.pick
.
A common practice for DI recipes to do this is to provide annotations. All of described annotations can be combined together. They are particularly implemented in Angular DI. Angular injector is available for standalone use (including Node) as injection-js
library.
Annotation property
This way function signature and the list of dependencies don't have to match. This recipe can be seen in action in AngularJS.
The property contains a list of DI tokens. They can be names of dependencies that will be loaded with require
or something else.
// may be more convenient when it's a string
const ANNOTATION = Symbol();
...
foo[ANNOTATION] = ['lodash'];
function foo(_) {
...
}
bar[ANNOTATION] = ['lodash'];
function bar() {
// doesn't need a param in signature
const _ = arguments[0];
...
}
And DI is performed like
const fnArgs = require('fn-args');
const annotation = foo[ANNOTATION] || fnArgs(foo);
foo(...annotation.map(depName => require(depName));
This style of annotations disposes to make use of function definitions, because hoisting allows to place annotation above function signature for convenience.
Array annotation
Function signature and the list of dependencies don't have to match. This recipe can be seen in AngularJS, too.
When function is represented as an array, this means that it is annotated function, and its parameters should be treated as annotations, and the last one is function itself.
const foo = [
'lodash',
function foo(_) {
...
}
];
...
const fn = foo[foo.length - 1];
const annotation = foo.slice(0, foo.length - 1);
foo(...annotation.map(depName => require(depName));
TypeScript type annotation
This recipe can be seen in Angular (2 and higher) and relies on TypeScript types. Types can be extracted from constructor signature and used for DI. Things that make it possible are Reflect
metadata proposal and TypeScript's own emitDecoratorMetadata
feature.
Emitted constructor types are stored as metadata for respective classes and can be retrieved with Reflect
API to resolve dependencies. This is class-based DI, since decorators are supported only on classes, it works best with DI containers:
import 'core-js/es7/reflect';
abstract class Dep {}
function di(target) { /* can be noop to emit metadata */ }
@di
class Foo {
constructor(dep: Dep) {
...
}
}
...
const diContainer = { Dep: require('lodash') };
const annotations = Reflect.getMetadata('design:paramtypes', Foo);
new (Foo.bind(Foo, ...annotations.map(dep => diContainer [dep]))();
This will produce workable JS code but will create type issues, because Lodash object isn't an instance of Dep
token class. This method is primarily effective for class dependencies that are injected into classes.
For non-class DI a fallback to other annotations is required.
npm init
defaults to1.0.0
. I'm not sure why, but I always just do that. – Colt