Typescript Generics: Inferring Generics in sub levels

36 views Asked by At

I have several types that I am working with that I would like to have inferred at the method call level.

class AbstractModal<TInput, TOutput> {
...
}

type Constructor<T> = new(...args: any[]) => T;

function openModal<TConstructor extends Constructor<AbstractModal<?, ?>>>(
    ctor: TConstructor,
    initialState?: TInput // TInput type for Constructor of Abstract Modal
): TOutput {
...
}

What I would like to be able to do is have a class defined as:

class TestModalType extends AbstractModal<string /*or another type*/, number /*or another type*/> {
...
}

and be able to call openModal with TestModalType as the generic parameter with the compiler getting upset at me for improper inputs on the initial call.

I have tried reading into these questions but have not found any help:

Typescript strict typing and inferred generic types

Inferring generic types in typescript

The specific problem that I am finding is that a lot of these answers are written for generics with only one generic parameter, where my situation requires a much more in depth example.

1

There are 1 answers

1
Alex Wayne On BEST ANSWER

You're probably better off drilling into the type you need from TConstructor, but in order to do that you need to know how TInput and TOutput are used.

So let's you start with this:

class AbstractModal<TInput, TOutput> {
  open(input: TInput) {
    return {} as TOutput // logic here
  }
}

Now you know that TInput is used as the first parameter to open() and TOutput is the return type of open().

Now you can write openModal like this:

function openModal<
  TConstructor extends Constructor<AbstractModal<unknown, unknown>>
>(
  ctor: TConstructor,
  initialState?: Parameters<InstanceType<TConstructor>['open']>[0],
): ReturnType<InstanceType<TConstructor>['open']> {
  return {} as any
}
  • TConstructor extends Constructor<AbstractModal<unknown, unknown>> constrains TConstructor to a Constructor that returns AbstractModal<unknown, unknown>. These unknowns will be made more specific by the extends when this is called.

  • Parameters<InstanceType<TConstructor>['open']>[0] gets the open method form the instance type that the constructor return, and uses the type of the first parameter.

  • ReturnType<InstanceType<TConstructor>['open']> gets the open method from the instance type that the constructor returns, and uses the return type.

And now this should work:

class TestModalType extends AbstractModal<string, number> {
  //...
}

const res1 = openModal(TestModalType, 'a string')
//    ^ number

Alternatively, if you want to infer the generics directly, you can do that, but it's quite a bit more verbose and cryptic. To do that you have to use a conditional type like:

A extends B<infer C> ? C : never

That might look like this:

function openModalInfer<TConstructor extends Constructor<TestModalType>>(
  ctor: TConstructor,
  initialState?: TConstructor extends Constructor<
    AbstractModal<infer TInput, unknown>
  > ? TInput : never,
): TConstructor extends Constructor<
    AbstractModal<unknown, infer TOutput>
  > ? TOutput : never {
  return {} as any
}

const res2 = openModalInfer(TestModalType, 'a string')
//    number

See Typescript Playground