---
url: https://masyaka.github.io/injecute/guide/registration.md
description: >-
  Lifetimes (singleton, transient, instance), dependency keys, optional
  dependencies, aliases, decoration and classes.
---

# 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](../playground?example=registration)

| Method                             | Created             | Cached          | Disposed by `dispose()`               |
| ---------------------------------- | ------------------- | --------------- | ------------------------------------- |
| `addSingleton(key, factory, deps)` | on first use        | yes             | yes (see [Lifecycle](./lifecycle.md)) |
| `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

```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](./tags.md));
* 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](./containers.md#forks). 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](./containers.md#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()`:

| 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](./containers.md#private-services)).
Registering the same key twice in one container throws
[`INJECUTE_ALREADY_REGISTERED`](../errors/already-registered.md) unless you pass `replace: true`. The
other way round, `replace: true` for a key that no container has throws
[`INJECUTE_NOTHING_TO_REPLACE`](../errors/nothing-to-replace.md): 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](./containers.md#namespaces) `Ns` | `namespace('Ns', …)`                                                          |
| `name:tag` | a service under a [tag](./tags.md); `name:start` is a lifecycle hook  | `tag('name')`, `lifecycle.start('name')` ([lifecycle](./startup-shutdown.md)) |

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