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.
The mistake happens in this line:
If
foosis justFoo[], this check works like expected. But whenfoosis optional, the type offoosisundefined | Foo[]which also will be the type ofT[K]. The-?operator does not excludeundefinedfromT[K]. It will only removeundefinedand?from the resulting mapped type which we only use for indexing.The union of
undefined | Foo[]does not extend(infer A)[], onlyFoo[]does. That's whyT[K]will also be passed to theKeysDeepin the false branch. You will see this massive union as a result which will happen when any array type is passed to theKeysDeeptype because using a mapped type on an array (and indexing it with its keys) will also return any method properties ofArray. You can see this happening here.One solution would be to use
Excludeto excludeundefinedfromT[K].Or you could copy
T[K]into a new type to distribute it.Playground