rsdi
Version:
TypeScript dependency injection container. Strong types without decorators.
140 lines (139 loc) • 6.89 kB
TypeScript
import { type ContainerLike, type DenyInputKeys, type Factory, type IDIContainer, type MergedResolvers, type ResolvedDependencies, type ResolvedDependencyValue, type Resolvers, type StringLiteral } from './types.js';
/**
* Dependency injection container
*/
export declare class DIContainer<ContainerResolvers extends ResolvedDependencies = {}> {
protected resolvedDependencies: {
[name in keyof ContainerResolvers]?: ResolvedDependencyValue;
};
protected resolvers: Resolvers<ContainerResolvers>;
private readonly context;
constructor();
/**
* 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.
*
* 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<T extends readonly ContainerLike[]>(...containers: T): IDIContainer<MergedResolvers<T>>;
/**
* Adds new dependency resolver to the container. If dependency with given name already exists it will throw an error.
* Use update method instead. It will override existing dependency.
* @param name
* @param resolver
*/
add<N extends string, V>(name: StringLiteral<DenyInputKeys<N, keyof ContainerResolvers>>, resolver: Factory<ContainerResolvers, V>): IDIContainer<ContainerResolvers & {
[n in N]: V;
}>;
/**
* 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.
*/
clone(): DIContainer<ContainerResolvers>;
export(): ResolvedDependencies;
/**
* Extends container with given function. It will pass container as an argument to the function.
* Function should return new container with extended resolvers.
* It is useful when you want to split your container into multiple files.
* You can create a file with resolvers and extend container with it.
* You can also use it to create multiple containers with different resolvers.
*
* For example:
*
* const container = new DIContainer()
* .extend(addValidators)
*
* export type DIWithValidators = ReturnType<typeof addValidators>;
* export const addValidators = (container: DIWithDataAccessors) => {
* return container
* .add('myValidatorA', ({ a, b, c }) => new MyValidatorA(a, b, c))
* .add('myValidatorB', ({ a, b, c }) => new MyValidatorB(a, b, c));
* };
* @param diConfigurationFactory
*/
extend<E extends (container: IDIContainer<ContainerResolvers>) => IDIContainer>(diConfigurationFactory: E): ReturnType<E>;
/**
* Resolve dependency by name. Alternatively you can use property access to resolve dependency.
* For example: const { a, b } = container;
* @param dependencyName
*/
get<Name extends keyof ContainerResolvers>(dependencyName: Name): ContainerResolvers[Name];
/**
* Checks if dependency with given name exists
* @param name
*/
has(name: string): boolean;
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.
* @param containers
*/
merge<T extends readonly ContainerLike[]>(...containers: T): IDIContainer<ContainerResolvers & MergedResolvers<T>>;
/**
* Updates existing dependency resolver. If dependency with given name does not exist it will throw an error.
* In most cases you don't need to override dependencies and should use add method instead. This approach will
* help you to avoid overriding dependencies by mistake.
*
* You may want to override dependency if you want to mock it in tests.
* @param name
* @param resolver
*/
update<N extends keyof ContainerResolvers, V>(name: StringLiteral<N>, resolver: Factory<ContainerResolvers, V>): IDIContainer<{
[n in N]: V;
} & {
[P in Exclude<keyof ContainerResolvers, N>]: ContainerResolvers[P];
}>;
protected setResolvers<CR extends ResolvedDependencies>(resolvers: Resolvers<CR>, resolvedDependencies: {
[name in keyof CR]: ResolvedDependencyValue;
}): void;
private addContainerProperty;
/**
* Sets value to the container
*/
private setValue;
}