3

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.

4
  • 2
    Please post an MVCE instead of a 3 page code dump. Commented Nov 5, 2020 at 14:49
  • I tried to simplify the problem, but it doesn't reproduce anymore, telling me Typscript cannot correctly traverse the indexing of the ActivityDefinitionBaseType['activities']. Commented Nov 5, 2020 at 14:55
  • You're right, thanks for the tips! Took me a bit for my mind to break out of its box. I've edited the example and it should be much more straightforward to understand now. Commented Nov 5, 2020 at 15:28
  • Edited to put the minimal reproducible example in the question itself instead of just the answer Commented Oct 6, 2023 at 19:34

1 Answer 1

5

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

Sign up to request clarification or add additional context in comments.

2 Comments

Whoa, you're awesome! Thanks for the detailed and quick answer, and the MVCE masterclass!
I've abstracted out this solution to make this a bit more readable: type IndexType<Type, Key extends string | number | symbol> = Extract<Type, { [K in Key]: unknown }>[Key] Used as IndexType<{ test: number }, 'test'>

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.