indexing a nested object with two dependent type parameters fails in typescript 39

Behold the power of a minimum reproducible example. I’m going to reduce this to a very small amount of code and change the names to protect the innocent. Given a type with nested keys like this:

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

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:

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]'

(If you squint you should be able to see this as trying to do ActivityStream[EntityType]['activities'][ActivityType]['request'])

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

Either way should be enough to convince the compiler that the generic indexing is valid.

Playground link to code

CLICK HERE to find out more related problems solutions.

Leave a Comment

Your email address will not be published.

Scroll to Top