Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't Typescript infer prop type when using Generics?

In the code below:

interface MyInterface {
   a: {
     b: {
       c: "c";
    };
  };
}

type ParentProps = keyof MyInterface
type ChildProps<ParentProp extends ParentProps> = keyof MyInterface[ParentProp]

type GrandChildType<ParentProp extends ParentProps, ChildProp extends ChildProps<ParentProp>> =
  MyInterface[ParentProp][ChildProp]['c']

I'm getting an error in the GrandChildType saying:

Type '"c"' cannot be used to index type 'MyInterface[ParentProp][ChildProp]'.(2536)

However, if I change the last line to:

type GrandChildType<ParentProp extends ParentProps, ChildProp extends ChildProps<ParentProp>> =
  MyInterface[ParentProp][ChildProp]

type test = GrandChildType<'a', 'b'>['c']

then I properly get the type of c which is "c". So, the question is: why can't I get that value type when using Generics if both ParentProps and ChildProps are keys extracted from MyInterface?

reproducible link: typescript playground

like image 439
Oswaldo Avatar asked Oct 27 '25 23:10

Oswaldo


1 Answers

This seems to be a bug in TypeScript; see microsoft/TypeScript#27709 for details. Nested generic indexed access types seem to "forget" their constraint and won't let you index into them with a specific key anymore. The bug has been open for a long time with no sign that it will be addressed soon, so you might want to work around it.

In cases where you have a type T where you know T extends U is true but the compiler doesn't, you can often replace T with Extract<T, U> using the Extract<T, U> utility type. The compiler will accept that Extract<T, U> extends U, and assuming you're correct about T extends U, then eventually (when the generics are specified), Extract<T, U> will evaluate to just T. (Note that this is an "off-label" use of Extract<T, U> which is used primarily to filter unions in T to just those members which are assignable to U.)

In order to index into some type T with the key type "c", it means the type should be assignable to {c?: any}. If T["c"] fails to compile, then Extract<T , {c?: any}> should succeed. Let's try it here:

type GrandChildType<K extends keyof MyInterface, K2 extends keyof MyInterface[K]> =
  Extract<MyInterface[K][K2], { c?: any }>['c']; // okay

type C = GrandChildType<"a", "b"> // "c"

Looks good! The type of GrandChildType<"a", "b"> is "c" as desired, and the error in GrandChildType is gone.

Playground link to code

like image 57
jcalz Avatar answered Oct 29 '25 16:10

jcalz