I am trying to implement a dependency injection system, so that instead of instantiating all the in-depth structure of dependencies manually …:
const config = new Config()
const serverStarter = new ServerStarter(config)
const application = new Application(serverStarter)
application.run()
… I can delegate to the DI context to instantiate entities for me whenever appropriate (potentially reusing them):
register(Config, [])
register(ServerStarter, [Config])
register(Application, [ServerStarter])
const application = instantiate(Application)
application.run()
I want to define the register and instantiate functions in as much of a type-safe manner as possible. But when I do, I get an error (in fact, I get many similar errors in many related places) that I don't know how to fix without explicit type casting:
interface Constructor<Instance extends object, Params extends readonly unknown[]> {
new (...args: Params): Instance
}
type ConstructorUnknown = Constructor<object, readonly never[]>
type ConstructorsUnknown = readonly ConstructorUnknown[]
type Instances<Constructors extends ConstructorsUnknown> = {
readonly [Index in keyof Constructors]: InstanceType<Constructors[Index]>
}
const entityConstructorToInputEntityConstructorsMap =
new Map<ConstructorUnknown, ConstructorsUnknown>()
function register<
const Entity extends object,
const InputEntityConstructors extends ConstructorsUnknown,
>(
entityConstructor: Constructor<Entity, Instances<InputEntityConstructors>>,
inputEntityConstructors: InputEntityConstructors,
): void {
if (entityConstructorToInputEntityConstructorsMap.has(entityConstructor)) {
// ~~~~~~~~~~~~~~~~~
// Argument of type 'Constructor<Entity, Instances<InputEntityConstructors>>' is not assignable to parameter of type 'ConstructorUnknown'.
// Types of parameters 'args' and 'args' are incompatible.
// Type 'readonly never[]' is not assignable to type 'Instances<InputEntityConstructors>'.
throw new Error(`Entity "${entityConstructor.name}" is already registered`)
}
entityConstructorToInputEntityConstructorsMap.set(entityConstructor, inputEntityConstructors)
// ~~~~~~~~~~~~~~~~~
// Argument of type 'Constructor<Entity, Instances<InputEntityConstructors>>' is not assignable to parameter of type 'ConstructorUnknown'.
}
If I cast entityConstructor to ConstructorUnknown, the error goes away:
entityConstructorToInputEntityConstructorsMap.has(entityConstructor as ConstructorUnknown)
// no errors
… but I would like to avoid doing that, because it usually means that there is a flaw in the type definitions, and I would rather fix the flaw.
How do I properly define the Constructor<…> generic?