rsdi
Version:
TypeScript dependency injection container. Strong types without decorators.
64 lines (63 loc) • 3.67 kB
TypeScript
import { type DIContainer } from './DIContainer.js';
/**
* Anything that can be composed/merged: a raw container instance or one that has
* already been widened by `add` (which returns `IDIContainer`).
*/
export type ContainerLike<R extends ResolvedDependencies = ResolvedDependencies> = DIContainer<R> | IDIContainer<R>;
export type DenyInputKeys<T, Disallowed> = T & (T extends Disallowed ? never : T);
export type Factory<ContainerResolvers extends ResolvedDependencies, Value = ResolvedDependencyValue> = (resolvers: ContainerResolvers) => Value;
export type IDIContainer<ContainerResolvers extends ResolvedDependencies = {}> = ContainerResolvers & {
add: <N extends string, V>(name: StringLiteral<DenyInputKeys<N, keyof ContainerResolvers>>, resolver: Factory<ContainerResolvers, V>) => IDIContainer<ContainerResolvers & {
[n in N]: V;
}>;
clone: () => IDIContainer<ContainerResolvers>;
extend: <E extends (container: IDIContainer<ContainerResolvers>) => IDIContainer>(f: E) => ReturnType<E>;
get: <Name extends keyof ContainerResolvers>(dependencyName: Name) => ContainerResolvers[Name];
has: (name: string) => boolean;
hasResolvedDependency: (name: string) => boolean;
merge: <T extends readonly ContainerLike[]>(...containers: T) => IDIContainer<ContainerResolvers & MergedResolvers<T>>;
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];
}>;
};
/**
* Collapses the resolver maps of a tuple of containers into a single map.
*
* Uses a union-to-intersection fold rather than a recursive tuple walk: a
* recursive fold trips TypeScript's instantiation depth limiter (TS2589) once a
* few dozen containers are combined, which silently degrades inference.
*/
export type MergedResolvers<T extends readonly unknown[]> = UnionToIntersection<ResolversOf<T[number]>> extends infer Merged ? Merged extends ResolvedDependencies ? Merged : {} : {};
export type ResolvedDependencies = {
[k: string]: ResolvedDependencyValue;
};
export type ResolvedDependencyValue = any;
export type Resolvers<CR extends ResolvedDependencies> = {
[k in keyof CR]?: Factory<CR>;
};
/**
* Extracts the resolver map from a container type. The class branch has to come
* first: a `DIContainer` instance also structurally matches `IDIContainer`.
*/
export type ResolversOf<C> = C extends DIContainer<infer R> ? R : C extends IDIContainer<infer R> ? R : never;
/**
* Gives a built container a name that survives into hovers and error messages.
*
* `typeof container` and `ReturnType<typeof configureDI>` both resolve to a type
* that already carries its own name, so TypeScript prints the whole resolver
* intersection instead of the alias — unreadable once a container has more than a
* handful of dependencies. Wrapping the container in this helper produces a fresh
* alias, so diagnostics print `AppContainer` rather than
* `IDIContainer<{ a: … } & { b: … } & …>`.
*
* export type AppContainer = SealedContainer<typeof container>;
*
* This is an ergonomic wrapper, not a performance one: resolving a container type
* in a consuming file is already cheap, and naming it costs marginally more.
* The dependency types themselves are unchanged.
*/
export type SealedContainer<C> = IDIContainer<ResolversOf<C>>;
export type StringLiteral<T> = T extends string ? (string extends T ? never : T) : never;
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;