Registering services
Every registration has a key, a factory and the keys it depends on. Dependencies are passed to the factory in the same order.
Lifetimes
const lifetimes = new DIContainer()
// created once, on first use, then cached
.addSingleton('clock', Clock)
// created again on every resolution
.addTransient('requestId', () => crypto.randomUUID())
// an existing value
.addInstance('config', { port: 8080 });
lifetimes.get('clock') === lifetimes.get('clock'); // true
lifetimes.get('requestId') === lifetimes.get('requestId'); // false| Method | Created | Cached | Disposed by dispose() |
|---|---|---|---|
addSingleton(key, factory, deps) | on first use | yes | yes (see Lifecycle) |
addTransient(key, factory, deps) | on every resolution | no | no |
addInstance(key, value) | by you | it is the value | only with { dispose: true } |
A factory is a function or a class. Classes are detected and called with new.
Dependencies
const withDependencies = new DIContainer()
.addInstance('config', { port: 8080, debug: false })
// keys are passed to the factory in the same order
.addSingleton('address', (config) => `http://localhost:${config.port}`, [
'config',
])
// optional(key): the service when it is registered, undefined otherwise
// a function dependency: its result, computed on each resolution
.addSingleton(
'banner',
(address, transport, startedAt) =>
`${address} via ${transport ?? 'console'} since ${startedAt}`,
['address', optional('transport'), () => new Date(0).toISOString()],
);A dependency is one of:
- a key: the factory receives that service;
optional(key): the service when it is registered,undefinedotherwise;collect(tag): every service registered under a tag, as an array (see Extension points with tags);- a function: called on every resolution, the factory receives its result.
When you need options, pass an object instead of the array:
app.addSingleton('mailer', createMailer, {
dependencies: ['config'],
replace: true, // replace an existing registration of 'mailer' in this container
dispose: (mailer) => mailer.close(), // how dispose() releases it
});Aliases
const logging = new DIContainer()
.addInstance('jsonLogger', { format: 'json' })
.addInstance('prettyLogger', { format: 'pretty' })
.addAlias(
'logger',
process.env.NODE_ENV === 'production' ? 'jsonLogger' : 'prettyLogger',
);
logging.get('logger'); // the pretty logger outside productionDecorating a service
A dependency on the key being registered receives its previous definition: the one it replaces in the same container, or the parent's one in a fork. Both need replace: true.
const decorated = new DIContainer()
.addInstance('greeting', 'hello')
// a dependency on the key being registered receives its previous definition
.addSingleton('greeting', (greeting) => greeting.toUpperCase(), {
replace: true,
dependencies: ['greeting'],
});
decorated.get('greeting'); // "HELLO"With replace: true the service's type becomes the new one, also when it changes (a string replaced by a number is a number afterwards), in a fork too:
const scope = app
.fork({ isolated: true })
.addInstance('port', 0, { replace: true }); // port: numberWhen a module replaces a service of the container it is applied to, the old and the new type are intersected; that is fine while the type stays the same (the usual decorator).
In a plain fork, a decoration (like any override) reaches only the services resolved in the fork: see Overriding a parent's service in a fork.
Classes that cannot be detected
The container recognises native classes (including subclasses, proxied classes and built-ins like Map). Two kinds of classes look like plain functions and cannot be detected:
- classes compiled to ES5: Babel for old browsers, TypeScript with
target: es5(removed in TypeScript 7), or prebuilt ES5 libraries; - bound classes (
MyClass.bind(null)).
Register them with construct(), which wraps the class in a factory that calls new:
// Classes compiled to ES5 (or bound classes) look like plain functions:
// register them with construct() so they are called with `new`.
function LegacyClient(this: { url: string }, url: string) {
this.url = url;
}
LegacyClient.prototype.ping = function () {
return 'pong';
};
const legacy = new DIContainer()
.addInstance('url', 'https://api.example.com')
.addSingleton(
'client',
construct(LegacyClient as unknown as new (url: string) => { url: string }),
['url'],
);What happens if you forget construct():
| Class | Without construct() |
|---|---|
Babel-compiled (_classCallCheck) | INJECUTE_CLASS_NOT_CONSTRUCTED |
TypeScript ES5 output that assigns this.x | INJECUTE_CLASS_NOT_CONSTRUCTED |
| ES5 class with prototype methods that returns nothing | INJECUTE_CLASS_NOT_CONSTRUCTED |
| Bound class | INJECUTE_CLASS_NOT_CONSTRUCTED (on V8, JavaScriptCore and SpiderMonkey) |
ES5 constructor with no methods that never uses this | resolves to undefined (cannot be detected) |
Keys
Keys are strings, numbers or symbols. Use symbols for services that should stay private to a module: in a namespace, they can't be resolved from outside it (see Private services). Registering the same key twice in one container throws INJECUTE_ALREADY_REGISTERED unless you pass replace: true. The other way round, replace: true for a key that no container has throws INJECUTE_NOTHING_TO_REPLACE: a replacement without an original usually means a registration is missing, such as a module that wasn't applied.
Reserved key forms
Two forms of string keys have a meaning. Build them with the helpers instead of typing them:
| Form | Meaning | Built by |
|---|---|---|
Ns.key | the service key of the namespace Ns | namespace('Ns', …) |
name:tag | a service under a tag; name:start is a lifecycle hook | tag('name'), lifecycle.start('name') (lifecycle) |
Other keys can contain . and : ('db:primary', 'config.db'), but avoid ending a key with : and the name of a tag or stage you use: collect() would collect it, and startLifecycle() would treat it as a hook (and reject it if it isn't a singleton). The two forms combine: Orders.list:route is the list:route service of the namespace Orders.