import 'reflect-metadata'; /** * Represents a class constructor type that can be instantiated with 'new' * @template T The type of instance the constructor creates */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Constructable = new (...args: any[]) => T; type AbstractConstructable = abstract new (...args: unknown[]) => T; type ServiceIdentifier = Constructable | AbstractConstructable; type Factory = (...args: unknown[]) => T; interface Metadata { instance?: T; factory?: Factory; } interface Options { factory?: Factory; } const instances = new Map(); /** * Decorator that marks a class as available for dependency injection. * @param options Configuration options for the injectable class * @param options.factory Optional factory function to create instances of this class * @returns A class decorator to be applied to the target class */ // eslint-disable-next-line @typescript-eslint/no-restricted-types export function Service(): Function; // eslint-disable-next-line @typescript-eslint/no-restricted-types export function Service(options: Options): Function; export function Service({ factory }: Options = {}) { return function (target: Constructable) { instances.set(target, { factory }); return target; }; } class DIError extends Error { constructor(message: string) { super(`[DI] ${message}`); } } class ContainerClass { /** Stack to track types being resolved to detect circular dependencies */ private readonly resolutionStack: ServiceIdentifier[] = []; /** * Checks if a type is registered in the container * @template T The type to check for * @param type The constructor of the type to check * @returns True if the type is registered (has metadata), false otherwise */ has(type: ServiceIdentifier): boolean { return instances.has(type); } /** * Retrieves or creates an instance of the specified type from the container * @template T The type of instance to retrieve * @param type The constructor of the type to retrieve * @returns An instance of the specified type with all dependencies injected * @throws {DIError} If circular dependencies are detected or if the type is not injectable */ get(type: ServiceIdentifier): T { const { resolutionStack } = this; const metadata = instances.get(type) as Metadata; if (!metadata) { // Special case: Allow undefined returns for non-decorated constructor params // when resolving a dependency chain (i.e., resolutionStack not empty) if (resolutionStack.length) return undefined as T; throw new DIError(`${type.name} is not decorated with ${Service.name}`); } if (metadata?.instance) return metadata.instance as T; // Add current type to resolution stack before resolving dependencies resolutionStack.push(type); try { let instance: T; const paramTypes = (Reflect.getMetadata('design:paramtypes', type) ?? []) as Constructable[]; const dependencies = paramTypes.map(

(paramType: Constructable

, index: number) => { if (paramType === undefined) { throw new DIError( `Circular dependency detected in ${type.name} at index ${index}.\n${resolutionStack.map((t) => t.name).join(' -> ')}\n`, ); } return this.get(paramType); }); if (metadata?.factory) { instance = metadata.factory(...dependencies); } else { // Create new instance with resolved dependencies instance = new (type as Constructable)(...dependencies) as T; } instances.set(type, { ...metadata, instance }); return instance; } catch (error) { if (error instanceof TypeError && error.message.toLowerCase().includes('abstract')) { throw new DIError(`${type.name} is an abstract class, and cannot be instantiated`); } throw error; } finally { resolutionStack.pop(); } } /** * Manually sets an instance for a specific type in the container * @template T The type of instance being set * @param type The constructor of the type to set. This can also be an abstract class * @param instance The instance to store in the container */ set(type: ServiceIdentifier, instance: T): void { // Preserve any existing metadata (like factory) when setting new instance const metadata = instances.get(type) ?? {}; instances.set(type, { ...metadata, instance }); } /** Clears all instantiated instances from the container while preserving type registrations */ reset(): void { for (const metadata of instances.values()) { delete metadata.instance; } } } /** * Global dependency injection container instance * Used to retrieve and manage class instances and their dependencies */ export const Container = new ContainerClass();