Pesquisa de site

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