How to properly define a generic constructor type in TypeScript?

16 Views Asked by At

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?

0

There are 0 best solutions below