export type Newable = { new(...args: any): T; } export type Factory = Newable; export type ServiceID = string | symbol | Factory; function isFactory(value: any): boolean { return typeof value === 'object' && value !== null && value instanceof Function } export function inject(target: any, key: PropertyKey, index: number) { if (!Reflect.hasMetadata('di:paramindices', target)) { Reflect.defineMetadata('di:paramindices', [], target) } const indices = Reflect.getMetadata('di:paramindices', target); indices.push(index); } function describeFactory(factory: Factory): string { return factory.name; } export class Container { private factories: Factory[] = []; private singletons = new Map(); public bindSelf(value: Factory | T): void { if (isFactory(value)) { this.factories.push(value as Factory); } else { this.singletons.set((value as T).constructor as ServiceID, value) } } public resolve(factory: Factory): T; public resolve(serviceId: ServiceID): any { return this.singletons.get(serviceId); } public createInstance(factory: Factory, ...args: any[]): T { const newArgs: any[] = []; const paramTypes = Reflect.getMetadata('design:paramtypes', factory); const indices = Reflect.getMetadata('di:paramindices', factory); if (paramTypes === undefined) { return new factory(...args); } let i = 0; for (const paramType of paramTypes) { if (indices.indexOf(i) !== -1) { newArgs.push(this.resolve(paramType)); } else { if (i >= args.length) { throw new Error(`Too few arguments provided to constructor of '${describeFactory(factory)}'.`); } newArgs.push(args[i++]); } } if (i < args.length) { throw new Error(`Too many arguments provided to factory function '${describeFactory(factory)}'.`); } return new factory(...newArgs); } }