import { INTERNAL_STATE, type InternalState } from './internalState.js'; import { type ContainerLike, type ContainerSnapshot, type DenyInputKeys, type Factory, type IDIContainer, type MergedResolvers, type ReservedName, type ResolvedDependencies, type ResolvedValues, type Resolvers, type StringLiteral, type UpdatedResolvers } from './types.js'; /** * Dependency injection container */ export declare class DIContainer { protected readonly [INTERNAL_STATE]: InternalState; /** * Combines independently built containers into a single new container. * * This is the recommended way to wire a large dependency graph. Splitting the graph * into modules and composing them is dramatically cheaper to type-check than one long * `add` chain, because each module chain is type-checked against its own small * resolver map instead of the ever-growing combined one: * * // repositories.ts * export const repositories = new DIContainer().add('userRepository', () => new UserRepository()); * // services.ts * export const services = new DIContainer().add('mailer', () => new Mailer()); * // container.ts * const container = DIContainer.compose(repositories, services); * * Factories may depend on names provided by any of the composed containers — resolution * happens lazily against the composed container, so cross-module dependencies work at * runtime. Only the *types* of a module are limited to what that module declares; when a * module needs another module's dependencies to be visible at compile time, layer them * with `extend` instead. A name no composed module provides throws * `DependencyIsMissingError` at resolution, naming the factory that asked for it. * * The inputs are left untouched: the composed container is a new instance. * * When several containers define the same name the last one wins at runtime, including * over a value the earlier container had already resolved. Note the types intersect rather * than overwrite, so a name defined twice with *different* types resolves to `never` — a * deliberate signal, since a scalable last-writer-wins type fold has to recurse per * container and trips TypeScript's depth limiter at ~50 of them. Prefer `update()` when a * replacement is intentional. * @param containers */ static compose(...containers: T): IDIContainer>; /** * Seeds a fresh container with copies of another's maps — what `clone()` does. `protected` so a * consumer's subclass constructor can call it too — which makes it an entry point for input the * types never saw, and so it runs the same checks `merge` runs, before writing anything: reserved * names, foreign own properties, non-function resolvers, and that every resolved value has a * resolver. Without them a seeded `get` shadowed the method and the first resolution overflowed * the stack, a seeded `42` failed at first `get` with V8's message, and a resolved value with no * resolver was a phantom dependency `has()` denied and `get()` returned. */ protected static seedResolvers(container: DIContainer, resolvers: Resolvers, resolvedDependencies: ResolvedValues): void; /** * Wires `container[name]` to `get(name)`. Called before the resolver is written, so that an own * property already under the name can be told apart: if the name has a resolver, the property is * the getter this method installed earlier — `update`, or `merge` of a name already held — and * there is nothing to do. If it has none, something else put that property there: a consumer * assignment, a subclass field, a factory writing through the deps object. Defining nothing and * carrying on used to leave the name half-working — `get(name)` ran the factory while * `container.name` returned the stray — so it is refused instead, before anything is written. */ private static addContainerProperty; /** * The checks a name has to pass before it can be registered, apart from whether it already is * one: not a container member, and not already an own property that something else put on the * container. `merge` runs it for every incoming name of every container before writing anything, * which is what makes it all-or-nothing. `add` and `update` check the reserved half inline and * leave the other to `addContainerProperty`, which has to look at the own property regardless. */ private static assertNameAvailable; /** * Builds the state object for a new container. Static so the proxy handler is written once; * the traps read the state back off the target at call time, so nothing here depends on * construction order. * * The proxy is what factories receive. Reads forward to the container, whose dependency getters * do the resolving — and a read of a name the container does not have throws instead of yielding * `undefined`. That is the whole reason the proxy exists: forwarding alone is what `this` already * does, and measured the same. The case it guards is the one the types cannot see — a module's * factory destructuring a name another module provides, composed without that module. Without * the trap that factory silently built its service around `undefined`. * * The `in` test walks the prototype chain on purpose: `toString`, `constructor`, and the * container's own methods are all reachable through the context, as they are on the container. * Anything that probes a protocol key the container lacks — `then` from `await deps`, `toJSON` * from `JSON.stringify(deps)` — throws; neither is a supported use of the context. * * Writes are refused outright. The proxy's target is the container, so `deps.scratch = 42` * inside a factory used to land as an own property on the container itself — invisible until a * later `add('scratch', …)` was refused for colliding with it — and `deps.a = 2` on a dependency * name failed with V8's own message about a getter-only property. A `TypeError`, as for a frozen * object, naming the key and the factory that was running. `preventExtensions` and * `setPrototypeOf` are refused too, or `Object.freeze(deps)` would freeze the container itself. * Only a write pays for the traps. * * The state is symbol-keyed and `ownKeys` leaves symbols out, so the deps object shows a factory * nothing but dependencies and the public methods: `deps.resolvers` is an unknown name like any * other, and `Object.getOwnPropertySymbols(deps)` is empty. Symbol *reads* still forward — the * container's own methods reach their state through `this`, which is the proxy when a factory * calls `deps.has('a')`. The trap hides the symbol only while it may: once the container is * frozen, sealed or made non-extensible, the proxy invariants require every own key to be * listed, and the symbol shows. */ private static createState; /** * The body of `merge`, shared with `compose` so that the error a bad argument produces can name * the method the consumer actually called; the argument positions are the same in both. */ private static mergeInto; /** * Stores a resolver under `name` and wires the property getter for it. Shared by `add` and * `update`; the name checks belong to the callers. */ private static setResolver; /** * Registers a factory under `name`. The factory runs once, on the first `get(name)` or * `container.name`, and its result is cached; it receives the container, so it can destructure * the dependencies it needs and they resolve lazily at that point. * * Throws `DenyOverrideDependencyError` if the name is already registered — use `update` to * replace on purpose — `ForbiddenNameError` if the name is a container member, and * `InvalidResolverError` if `resolver` is not a function. The first two are also compile * errors: a registered or reserved name types the parameter as `never`. * * Returns the same container with `name` added to its type, so the calls chain. * @param name an inline string literal; a widened `string` is rejected at compile time * @param resolver a function of the container's dependencies to the value */ add(name: StringLiteral>, resolver: Factory): IDIContainer; /** * Creates a new container instance with the same resolvers. * * Useful when you want to share a base container across different modules. * For example, you can define a base container with shared dependencies, * then clone it to create separate DI configurations for different bounded contexts. * * The cloned container is a new instance but retains all the original resolvers. * * Typed as `IDIContainer`, like every other method that hands the container back, so property * access stays typed on the result. `DIContainer` alone does not intersect the resolver map — * on a subclass instance `clone().foo` was a `TS2339` while `IDIContainer` said it existed. */ clone(): IDIContainer; /** * Returns the container's resolvers and its already-resolved values. * * Both maps are copies. `add`, `update` and `merge` write into the internal maps in place, so * handing out the live objects would let a caller both observe registrations made after the * call and mutate the container by writing into what they were given. Nothing inside the class * goes through here — `clone` and `merge` read the state directly — so the copy is paid only by * a consumer that asks for it. * * Declared on `IDIContainer` as well, so it stays reachable after `add` has widened the type. */ export(): ContainerSnapshot; /** * Passes the container to `diConfigurationFactory` and returns whatever it returns. This is how * a module layers on top of an earlier one when its factories need the earlier dependencies to * be visible at compile time — `compose` combines modules but keeps each module's types to * itself. * * // validators.ts * export const addValidators = (container: DIWithDataAccessors) => * container * .add('validatorA', ({ a, b }) => new ValidatorA(a, b)) * .add('validatorB', ({ a, c }) => new ValidatorB(a, c)); * * // container.ts * const container = dataAccessors.extend(addValidators); * * Give module functions an explicit return type when chaining several; `docs/ai-agent-guide.md` * explains why `ReturnType` accumulates depth. * @param diConfigurationFactory receives this container, typed with its current dependencies */ extend) => IDIContainer>(diConfigurationFactory: E): ReturnType; /** * Resolves a dependency by name. `container.name` and destructuring the container are the same * call. The factory runs on the first request and the value is cached; a cache hit is one map * lookup. * * Throws `DependencyIsMissingError` if nothing is registered under the name and * `CircularDependencyError` if resolving it leads back to itself; both messages carry the * resolution path when the request came from inside a factory. * @param dependencyName a registered name */ get(dependencyName: Name): ContainerResolvers[Name]; /** * Whether a resolver is registered under `name`, resolved or not. Takes any string, so it can * probe a name the type does not know about. * @param name */ has(name: string): boolean; /** * Whether `name` has been resolved and cached. `false` for a registered name nothing has asked * for yet, and again after `update` replaces its resolver. Takes any string, like `has`. * @param name */ hasResolvedDependency(name: string): boolean; /** * Merges other containers into this one. Resolved dependencies are merged as well. * * Accepts any number of containers, so a set of independently built modules can be * combined in a single call: * * base.merge(repositories, services, controllers) * * Combining modules this way is also much cheaper to type-check than one long * `add` chain — see docs/type-performance-plan.md. * * When several containers define the same name the last one wins at runtime, including over * an already-resolved value. The types intersect rather than overwrite, so the same name with * two different types resolves to `never` rather than the later type. * * This mutates and returns `this`; use `clone()` or the static `DIContainer.compose()` * when a separate instance is required. Every incoming name is checked before anything is * written, so a merge that throws leaves this container exactly as it was. * @param containers */ merge(...containers: T): IDIContainer>; /** * Replaces the resolver registered under `name` and evicts its cached value, so the next request * runs the new factory. Throws `DependencyIsMissingError` if the name is not registered — `add` * is for new names, and keeping the two apart is what stops a dependency being redefined by * accident. The usual reason to call this is a test double. * * Safe to call while the name's own factory is running: the value that factory produces is * handed to whoever asked for it but not cached, so the next request runs the replacement. * * Chaining overrides off a built container is a supported shape and stays cheap: when * the replacement has the same type as the dependency it replaces — a test double for * the real service — the container type passes through unchanged, so the chain costs * the same at 60 links as at 20. See `UpdatedResolvers` in `types.ts`. * @param name * @param resolver */ update(name: StringLiteral, resolver: Factory): IDIContainer>; }