---
url: https://masyaka.github.io/injecute/migration/0.x-to-1.0.md
description: >-
  Every breaking change between injecute 0.x and 1.0, with before/after
  examples.
---

# Migrating from 0.x to 1.0

injecute 1.0 cleans up the public API. This page lists every breaking change with a before/after example.
Work through it top to bottom; most changes are mechanical.

**Checklist**

1. Use Node.js ≥ 22.12 and `import { DIContainer } from 'injecute'` (no default export, ESM only).
2. Replace `IDIContainer` in module signatures with `ServiceRegistry<{ …what the module needs }>`, and
   consumers' container parameters with `ServiceProvider<{ … }>`.
3. Drop `construct()` for native classes (keep it for ES5-compiled and bound classes).
4. Replace `'undefined'` dependency keys with `optional(key)` or `() => undefined`.
5. Rename `allowUnresolved` → `optional`, `has(key, false)` → `has(key, { local: true })`,
   `new DIContainer({ parentContainer })` → `parent.fork()`.
6. Update middlewares to `(key, next, { container })` and `fork({ skipResolvers })` to
   `fork({ middlewares: false })`.
7. Replace `flatten()` with `fork({ isolated: true })`.
8. Branch on `InjecuteError.code` instead of error messages; factory errors now arrive wrapped (the
   original is `cause`).
9. `await preload(app)`: it returns a promise now.
10. Replace `ContainerServices<typeof app>['Billing']` (a namespace's container type) with
    `NamespaceServices<typeof app, 'Billing'>`.
11. Pass `{ replace: true }` where a fork, or a namespace callback, registers a key its parent has, and
    only there: `replace: true` for a key that nobody has throws.

## Module format: ESM only

1.0 ships a single ES module build. The CommonJS build is gone.

* **Node.js ≥ 22.12** is required. `require('injecute')` keeps working through Node's `require(esm)`
  support, which 22.12 is the first 22.x release to enable without a flag.
* Browsers, Deno and Bun import the package directly, including from CDNs such as esm.sh and jsDelivr.
* Bundlers need no configuration.

```ts
// before (CommonJS)
const { DIContainer } = require('injecute');

// after (ESM)
import { DIContainer } from 'injecute';

// still works on Node ≥ 22.12 (require of an ES module)
const { DIContainer } = require('injecute');
```

## The `utils` object is removed

Every helper was already exported by name. The grouped `utils` object duplicated them and broke the
published type declarations for projects with `skipLibCheck: false`.

```ts
// before
import { utils } from 'injecute';
utils.construct(MyClass);

// after
import { construct } from 'injecute';
construct(MyClass);
```

## `null` and `undefined` are valid service values

0.x treated a factory result of `null` or `undefined` as "not registered": `get()` threw
`No service registered`, and singletons returning `null` ran their factory again. 1.0 looks services up by
key, so any value, including `null` and `undefined`, is returned and cached.

```ts
const c = new DIContainer().addSingleton('session', () => null, []);
c.get('session'); // 0.x: throws · 1.0: null
```

## Decorating a service with a self dependency

A dependency on the key being registered now resolves the **previous definition** of that key: the
replaced one in the same container (with `replace: true`), or the parent's one in a child. In 0.x a child
could not do this at all.

```ts
const child = parent
  .fork()
  .addSingleton('logger', (logger) => withPrefix(logger), {
    dependencies: ['logger'],
    replace: true, // required in a fork too, see below
  });
```

Re-registering an own key without `replace: true` throws `already registered` (0.x threw
`CircularDependencyError` when the new registration depended on itself).

## A fork needs `replace: true` to override a parent's key

In 0.x a fork could register a key its parent has without `replace`. Services registered in the parent
kept the parent's instance, and nothing said so. 1.0 throws `INJECUTE_ALREADY_REGISTERED` and names those
services; pass `{ replace: true }` when the override is meant for the fork's own services only.

```ts
// 0.x
const scope = app.fork().addInstance('logger', requestLogger);
// 1.0
const scope = app
  .fork()
  .addInstance('logger', requestLogger, { replace: true });
```

When a parent service should use the fork's instance, register that service in the fork too, or use
`fork({ isolated: true })` to replace it for every service. See
[Overriding a parent's service in a fork](../guide/containers.md#overriding-a-parent-s-service-in-a-fork).
The same applies to `namespace()` callbacks, which receive a fork. `addAlias()` takes
`{ replace: true }` now too.

The other way round, `replace: true` for a key that no container has throws
`INJECUTE_NOTHING_TO_REPLACE` (0.x registered it as a new service). Remove `replace: true` from first
registrations, or find the registration that is missing.

## Middlewares run once per `get()`

In 0.x a fork copied its parent's middlewares, and resolving a parent service ran them again at every
level, so one `get()` through two forks ran a middleware three times. 1.0 runs the chain once per `get()`.

## Middleware signature: `(key, next, context)`

Middlewares no longer receive the container as `this`, so arrow functions work. The container and the
resolution path are on the third argument. `next()` continues with the same key; `next(otherKey)`
resolves another key.

```ts
// before
container.use(function (key, next) {
  this.get('logger').debug(String(key));
  return next(key);
});

// after
container.use((key, next, { container, path }) => {
  container.get('logger').debug(path.join(' > '));
  return next();
});
```

Forks inherit their ancestors' middlewares **live**: a middleware added to a parent after `fork()` applies
to existing forks. `fork({ skipResolvers: true })` is now `fork({ middlewares: false })`. Remove a
middleware with `unuse(middleware)`.

## `flatten()` is removed

`flatten()` threw with its default options and, when forced to run, cached the flattened container's
singletons in the root container. Use an isolated fork instead: it runs every factory it resolves in the
fork, so overrides reach the whole graph and nothing leaks back.

```ts
// before
const copy = app.flatten({ onKeyIntersection: (k) => () => undefined });

// after
const copy = app.fork({ isolated: true });
```

## Introspection: `getRegistration()` replaces `getFactory()`

`getFactory()`, `entryTypeKey`, `firstResult` and the `EntryType` type are removed. Read registration
metadata with `getRegistration(key)`, which returns a read-only `RegistrationInfo` (`kind`,
`dependencies`, `target`, `namespace`, `depth`). `EntryType` is now `RegistrationKind`.

The `replace` event payload reports the replaced registration as `previous: RegistrationInfo` instead of
`replaced: { callable, type }`.

## Registration hooks are removed

The undocumented `beforeResolving`, `afterResolving` and `beforeReplaced` registration options are gone.
Use the `produce` / `replace` events or a middleware instead.

## Container roles: `ServiceProvider`, `ServiceRegistry`, `DIContainer`

1.0 splits the container into three levels. Each piece of code gets only what it needs:

| Type                 | Can                                                                                             | Give it to                                                       |
| -------------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `ServiceProvider<S>` | resolve (`get`, `has`, `createResolver`, `call`) and inspect (`keys`, `getRegistration`)        | handlers and other consumers, middleware context, event payloads |
| `ServiceRegistry<S>` | everything above, plus register (`add*`, `addAlias`, `namespace`, `extend`, `injecute`, `bind`) | module / extension functions                                     |
| `DIContainer<S>`     | everything above, plus `use`/`unuse`, events, `fork`, `reset`, `dispose`                        | the composition root                                             |

* `ServiceRegistry<S, A>` keeps the services it was given (`S`) apart from the ones it added (`A`), and
  resolves both. You rarely write `A`; it shows up in inferred module types, for example
  `ServiceRegistry<{ db: Database }, { invoices: InvoiceRepository }>`. A container is a registry that
  was given nothing: `DIContainer<S>` is a `ServiceRegistry<{}, S>`.
* `IDIContainer<S>` still exists as a deprecated alias of `ServiceRegistry<{}, S>`.
* The chain keeps its type: `new DIContainer().addInstance(…).use(…)` compiles (0.x lost `use()` after
  the first `add*`).
* `get('SomeNamespace')`, `getParent()` and the middleware/event `container` are read-only providers.
* `DIContainer<TOwn, TParent>` has one type parameter now: `DIContainer<S>`.

Modules no longer need to be generic. Declare only what the module needs and apply it with `extend()`:

```ts
// before
const addBilling = <T extends { db: Database }>(c: IDIContainer<T>) =>
  c.addSingleton('invoices', construct(InvoiceRepository), ['db']);

// after
const addBilling = (c: ServiceRegistry<{ db: Database }>) =>
  c.addSingleton('invoices', InvoiceRepository, ['db']);

app.extend(addBilling); // type error if `db` is not registered
```

## Namespace types

A namespace is typed now. `get('Billing')` returns a `ServiceProvider` of the namespace's services
instead of an untyped container, and `NamespaceServices<C, 'Billing'>` returns those services.

In the service map, the namespace name holds a `Namespace` marker (`AsyncNamespace` in an
`AsyncDIContainer`) instead of a container type. The map keeps every namespaced service once, under its
full key, so nested namespaces don't multiply the type and exported containers keep small `.d.ts` files.

```ts
// before: an IDIContainer<Record<ArgumentsKey, any>>
type Billing = ContainerServices<typeof app>['Billing'];

// after
type Billing = NamespaceServices<typeof app, 'Billing'>; // { invoices: InvoiceRepository }
app.get('Billing'); // ServiceProvider<{ invoices: InvoiceRepository }>
app.get('Billing.invoices'); // InvoiceRepository, as before
```

`ServiceType<S, K>` is what resolving `K` from a service map `S` returns: the registered type, or a
provider for a namespace key.

## Classes are registered directly

Classes are detected and constructed with `new`; `construct()` is optional.

```ts
// before
app.addSingleton('users', construct(UserRepository), ['db']);

// after
app.addSingleton('users', UserRepository, ['db']);
```

Keep `construct()` for classes compiled to ES5 (Babel for old browsers, TypeScript `target: es5`) and bound
classes: they look like plain functions and cannot be detected.

## Optional dependencies: `optional(key)`

The magic `'undefined'` dependency key is gone (and `'undefined'` is an ordinary key now). Use
`optional(key)` for "the service if it is registered", or a function dependency for a fixed value.

```ts
// before
app.addSingleton('mailer', construct(Mailer), ['undefined']);

// after
app.addSingleton('mailer', Mailer, [optional('transport')]);
app.addSingleton('mailer', Mailer, [() => undefined]);
```

## Renamed options and removed constructor argument

| 0.x                                            | 1.0                                                    |
| ---------------------------------------------- | ------------------------------------------------------ |
| `get(key, { allowUnresolved: true })`          | `get(key, { optional: true })`                         |
| `has(key, false)`                              | `has(key, { local: true })`                            |
| `new DIContainer({ parentContainer: parent })` | `parent.fork()`                                        |
| `createProxyAccessor(c, { allowUnresolved })`  | `createProxyAccessor(c, { optional })`                 |
| `import DIContainer from 'injecute'`           | `import { DIContainer } from 'injecute'`               |
| `ArgumentsKey`                                 | `ServiceKey` (deprecated alias kept)                   |
| `DependenciesToTypes<Keys, S>`                 | `ResolveDependencies<Keys, S>` (deprecated alias kept) |
| `Events<C>`                                    | `ContainerEvents`                                      |

`createProxyAccessor()` objects now support `in`, `Object.keys()` and spreading; writes throw
`The proxy accessor is read-only.`

## Removed type exports

These helper types are gone; most were internal or unused: `Callable`, `Func`, `Constructor`,
`CallableResult`, `ValueOf`, `Empty`, `Flatten`, `Merge`, `KeyForValueOfType`, `ValuesOfType`,
`ArgumentsTypes`, `Dependencies`, `TypesToKeys`, `PromisedProperties`, `CallableOf`, `ArgumentsResolver`,
`InjecuteOptions`, `WithNamespace`, `MapOf`, `ResolversMapKeys`, `Resolve`, `Resolver`,
`IDIContainerExtension`, `ContainerOwnServices`, `ContainerParentServices`,
`DIContainerConstructorArguments`, `OptionalDependencySkipKey` and `optionalDependencySkipKey`.

Use `Factory<D, S>`, `Produced<F>`, `ResolveDependencies<D, S>`, `ContainerServices<C>` and the option
types (`SingletonOptions`, `InstanceOptions`, `ForkOptions`, …) instead. A module function is simply
`(c: ServiceRegistry<{ … }>) => …`.

## Errors: `InjecuteError` with codes

Every error injecute throws is an `InjecuteError` with a stable `code` (for example
`INJECUTE_NOT_REGISTERED`), the resolution `path`, a `docs` link, and a message that says how to fix it.
`CircularDependencyError` is a subclass. Branch on `code` instead of matching message text; messages
changed and may change again.

```ts
try {
  app.get('users');
} catch (error) {
  if (
    error instanceof InjecuteError &&
    error.code === 'INJECUTE_NOT_REGISTERED'
  ) {
    // error.path: ['users', 'db'], error.docs: https://masyaka.github.io/injecute/errors/not-registered
  }
}
```

Errors thrown by your factories, and rejections of the promises they return, are wrapped in
`INJECUTE_RESOLUTION_FAILED` with the original error as `cause`, so the message shows which service
needed it. Check `error.cause` if you relied on catching your own error types from `get()`. `dispose()` failures reject with `INJECUTE_DISPOSE_FAILED` (the
individual errors are in `cause`, an `AggregateError`).

## `preload()` returns a promise

`preload()` now returns a promise that settles once async services (factories that return promises) are
created, and rejects with the first failure. In 0.x a failing async service became an unhandled
rejection. Synchronous failures still throw immediately.

```ts
// before
preload(app);

// after
await preload(app);
```

## Async services

A singleton whose factory rejected is no longer cached: the next `get()` calls the factory again.
`dispose()` releases the resolved value of async singletons.

`AsyncDIContainer` was never exported in 0.x (an `import { AsyncDIContainer } from 'injecute'` failed).
In 1.0 it is public, with its own module and consumer types; see [Async services](../guide/async.md).
