Documents injecute 1.x · Migrating from 0.x
Skip to content

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 ​

ts
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

Open in the playground

MethodCreatedCachedDisposed by dispose()
addSingleton(key, factory, deps)on first useyesyes (see Lifecycle)
addTransient(key, factory, deps)on every resolutionnono
addInstance(key, value)by youit is the valueonly with { dispose: true }

A factory is a function or a class. Classes are detected and called with new.

Dependencies ​

ts
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, undefined otherwise;
  • 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:

ts
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 ​

ts
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 production

Decorating 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.

ts
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:

ts
const scope = app
  .fork({ isolated: true })
  .addInstance('port', 0, { replace: true }); // port: number

When 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:

ts
// 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():

ClassWithout construct()
Babel-compiled (_classCallCheck)INJECUTE_CLASS_NOT_CONSTRUCTED
TypeScript ES5 output that assigns this.xINJECUTE_CLASS_NOT_CONSTRUCTED
ES5 class with prototype methods that returns nothingINJECUTE_CLASS_NOT_CONSTRUCTED
Bound classINJECUTE_CLASS_NOT_CONSTRUCTED (on V8, JavaScriptCore and SpiderMonkey)
ES5 constructor with no methods that never uses thisresolves 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:

FormMeaningBuilt by
Ns.keythe service key of the namespace Nsnamespace('Ns', …)
name:taga service under a tag; name:start is a lifecycle hooktag('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.

Released under the MIT License.