Home > database >  Recursively get all keys using mapped type
Recursively get all keys using mapped type

Time:07-28

I know this question has been asked before, and that there are working approaches, such as this, but I have a specific question about why a certain implementation using mapped types does not work:

When trying to recursively get all keys of a nested object, you need to handle the case of an array, so I wrote this type:

type KeysDeep<T> = T extends object
  ? {
      [K in keyof T]-?:
        | K
        | (T[K] extends (infer A)[] ? KeysDeep<A> : KeysDeep<T[K]>);
    }[keyof T]
  : never;

And this seems to work well, except if a property with an array type is optional, e.g.

type Foo = {
    foo: string;
}

type Bar = {
    bar: string;
    foos: Foo[];
}

type Baz = {
    baz: string;
    maybeFoos?: Foo[];
}

type Good = KeysDeep<Bar>; // "foos" | "bar" | "foo"
type Bad = KeyDeep<Baz>; // "foos" | "bar" | "foo" | ... a zillion more

I would have thought the ?- operator would have adequately handled the optional property here. I've tried modifying this type in several ways but have been unable to make it work. Is there any way to fix this issue?

In the end, it's fine to use the solution I linked above, however it has poorer intellisense. e.g. the intellisense tooltip will list something like "foo" | keyof Bar instead of "foos" | "bar" | "foo".

Anyway, this isn't a huge deal, but I'm just curious if there's a fix and as to why this doesn't work. Thanks in advance.

Here's a playground link with a full example.

CodePudding user response:

The mistake happens in this line:

(T[K] extends (infer A)[] ? KeysDeep<A> : KeysDeep<T[K]>)

If foos is just Foo[], this check works like expected. But when foos is optional, the type of foos is undefined | Foo[] which also will be the type of T[K]. The -? operator does not exclude undefined from T[K]. It will only remove undefined and ? from the resulting mapped type which we only use for indexing.

The union of undefined | Foo[] does not extend (infer A)[], only Foo[] does. That's why T[K] will also be passed to the KeysDeep in the false branch. You will see this massive union as a result which will happen when any array type is passed to the KeysDeep type because using a mapped type on an array (and indexing it with its keys) will also return any method properties of Array. You can see this happening here.

One solution would be to use Exclude to exclude undefined from T[K].

(Exclude<T[K], undefined> extends (infer A)[] ? KeysDeep<A> : KeysDeep<T[K]>)

Or you could copy T[K] into a new type to distribute it.

(T[K] extends infer U 
  ? U extends (infer A)[] 
    ? KeysDeep<A> 
    : KeysDeep<T[K]> 
  : never
);

Playground

  • Related