typescript - Obter recursivamente todas as chaves usando tipo mapeado
Eu sei que essa pergunta já foi feita antes, e que existem abordagens que funcionam, como esta, mas eu tenho uma pergunta específica sobre por que uma certa implementação usando tipos mapeados não funciona:
Ao tentar obter recursivamente todas as chaves de um objeto aninhado, você precisa lidar com o caso de um array, então eu escrevi este tipo:
type KeysDeep<T> = T extends object
? {
[K in keyof T]-?:
| K
| (T[K] extends (infer A)[] ? KeysDeep<A> : KeysDeep<T[K]>);
}[keyof T]
: never;
E isso parece funcionar bem, exceto se uma propriedade com um tipo de array for opcional, por exemplo.
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
Eu teria pensado que o operador ?- teria lidado adequadamente com a propriedade opcional aqui. Eu tentei modificar este tipo de várias maneiras, mas não consegui fazê-lo funcionar. Existe alguma maneira de resolver esse problema?
No final, é aceitável usar a solução que linkei acima, no entanto, ela possui uma intellisense inferior. Por exemplo, a dica de intellisense mostrará algo como "foo" | keyof Bar ao invés de "foos" | "bar" | "foo".
De qualquer forma, isso não é um grande problema, mas estou apenas curioso se há uma solução e por que isso não funciona. Obrigado antecipadamente.
Aqui está um link do playground com um exemplo completo.
Resposta #1
O erro acontece nesta linha:
(T[K] extends (infer A)[] ? KeysDeep<A> : KeysDeep<T[K]>)
Se foos for apenas Foo[], essa verificação funciona como esperado. Mas quando foos é opcional, o tipo de foos é undefined | Foo[], que também será o tipo de T[K]. O operador -? não exclui undefined de T[K]. Ele apenas remove undefined e ? do tipo mapeado resultante, que usamos apenas para indexação.
A união de undefined | Foo[] não se estende a (infer A)[], apenas Foo[] se estende. É por isso que T[K] também será passado para o KeysDeep no ramo falso. Você verá essa união massiva como resultado, o que acontecerá quando qualquer tipo de array for passado para o tipo KeysDeep, porque usar um tipo mapeado em um array (e indexá-lo com suas chaves) também retornará qualquer propriedade de método de Array. Você pode ver isso acontecendo aqui.
Uma solução seria usar Exclude para excluir undefined de T[K].
(Exclude<T[K], undefined> extends (infer A)[] ? KeysDeep<A> : KeysDeep<T[K]>)
Ou você poderia copiar T[K] para um novo tipo para distribuí-lo.
(T[K] extends infer U
? U extends (infer A)[]
? KeysDeep<A>
: KeysDeep<T[K]>
: never
);
Parque infantil
