Vincent den Boer
Vincent den Boer

Reputation: 77

Indexing a nested object with two dependent type parameters fails

The code below fails with a Type '"d"' cannot be used to index type 'Foo[A]["b"][C]' error. It seems that you cannot use string literal keys to index into a type that is already the result of multiple nested generic lookups. After the second generic keyof, the compiler complains:

interface Foo {
  a: {
    b: {
      c: {
        d: string
      };
    };
  };
}

type Bar<A extends keyof Foo, C extends keyof Foo[A]['b']> =
  Foo[A]['b'][C]['d']; // error!
// Type '"d"' cannot be used to index type 'Foo[A]["b"][C]'

Why can I not index this? Weirdly enough, the code that consumes this does work correctly.

Upvotes: 3

Views: 168

Answers (1)

jcalz
jcalz

Reputation: 327594

This is a known bug (or possibly design limitation) in TypeScript, and there is an open issue for it at microsoft/TypeScript#21760. According to a language designer, the first generic lookup widens the index constraint to string and then the second one doesn't have the necessary context.

Note that when you specify the generics with specific keys, the compiler is able to understand the lookup type so it still works for anyone using the type:

type WorksThough = Bar<"a", "c"> // string

Anyway, I guess there was a brief attempt to fix #21760, which broke other things, so it couldn't be used. The issue has been languishing since then. It currently remains on the issue backlog, so you probably can't expect to see it fixed anytime soon.


Instead, you could, as a workaround, give the compiler a little more explicit context. If Foo[A]['b'][C] isn't known to have a d key, you can tell it so by changing the type to Extract<Foo[A]['b'][C], {d: unknown}> (with unknown replaced with something more specific if you know it):

type Baz<A extends keyof Foo, C extends keyof Foo[A]['b']> =
  Extract<Foo[A]['b'][C], { d: unknown }>['d'];

type AlsoWorks = Baz<"a", "c"> // string

The Extract<T, U> utility type is usually used to take a union type T and return only those pieces of it assignable to U. If T is not a union type and is definitely assignable to U, then Extract<T, U> will evaluate to T, but the compiler sees Extract<T, U> as assignable to U also. It is also possible to use an intersection for this instead:

type Qux<A extends keyof Foo, C extends keyof Foo[A]['b']> =
  (Foo[A]['b'][C] & { d: unknown })['d'];

type StillWorks = Qux<"a", "c"> // string

Or, depending on the use case, you might find conditional type inference easier:L

type Quux<A extends keyof Foo, C extends keyof Foo[A]['b']> =
  Foo[A] extends { b: Record<C, { d: infer D }> } ? D : never

type StillStillWorks = Quux<"a", "c"> // string

Any of those ways should be enough to convince the compiler that the generic indexing is valid.

Playground link to code

Upvotes: 4

Related Questions