Search code examples
typescriptgenericsnarrowing

Typescript Expands Narrowed Generic Type


I have this code:

type Constructable = {new(...args: any[]): any, prototype: any}; // Represents a class (that can be called with new)

type WithOnlyType<O, T> = {[P in keyof O as O[P] extends T? P: never]: O[P]}; // https://stackoverflow.com/questions/51419176/how-to-get-a-subset-of-keyof-t-whose-value-tk-are-callable-functions-in-typ

// Override a constructor with name C in a scope S
function extendConstructor<
    S,
    T extends WithOnlyType<S, Constructable>,
    C extends keyof T>
    (
        className: C, constructor: (...args: ConstructorParameters<T[C]>) => any, scope: S) {

...}

However, ConstructorParameters<T[C]> is erroring because Type 'T[C]' does not satisfy the constraint 'abstract new (...args: any) => any'. Which is odd, because T contains only values that fit that requirement (are Constructable) and C is keyof T. However, farther down (at the bottom of) the error, I found:

Type 'S[string]' is not assignable to type 'abstract new (...args: any) => any'

This is true, S could have a lot more values than just classes, however, T is a lot more narrowed than S, and C than string. How to I stop tsc from expanding these types?


Solution

  • This is a design limitation of TypeScript. See microsoft/TypeScript#30728 for the description of a similar issue. The compiler is not clever enough to do the sort of higher-order analysis necessary to figure out that T[C] will be assignable to Constructable, when T and C are both unspecified generic type parameters. You wrote WithOnlyType specifically to guarantee this sort of constraint, but WithOnlyType<S, Constructable>[keyof WithOnlyType<S, Constructable>] is more or less opaque to the compiler.

    If you want to work around this, you can use the Extract<T, U> utility type. When you have a type T that you know is assignable to type U but the compiler doesn't know this, you can write Extract<T, U> instead of T. The compiler sees that Extract<T, U> is assignable to U, and you know that Extract<T, U> will eventually resolve to just whatever T is (because Extract<T, U> evaluates to any union members of T which are assignable to U).

    So if you change

    ConstructorParameters<T[C]>
    

    to

    ConstructorParameters<Extract<T[C], Constructable>>
    

    the error will go away.

    Playground link to code