Reputation: 77
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
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.
Upvotes: 4