Eu tenho um number[] | string[]
e quero filtrá-lo. O problema é que a função de filtro muda o tipo para (number | string)[]
instead.
// list of numbers OR list of strings
let myList = Math.random() < 0.5 ? [1, 2, 3] : ["a", "b", "c"];
// filter the list
const filtered = myList.filter((x) => { return true; });
// oh no, the result is Array<number | string> instead of (number[] | string[])
myList = filtered;
Erro na última linha:
Type '(string | number)[]' is not assignable to type 'number[] | string[]'.
Type '(string | number)[]' is not assignable to type 'number[]'.
Type 'string | number' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.(2322)
Como posso expressar o fato de que a matriz não pode ser mista , mas pode conter qualquer tipo de elemento?
O TypeScript não distribui automaticamente os tipos de retorno de chamadas de função/método genéricos entre uniões em seus argumentos. Conceitualmente, uma função do formulário
<T>(i: I<T>)=>O<T>
quando chamada com um argumento do tipoI<T1> | I<T2> | I<T3>
deve produzir uma saída do tipoO<T1> | O<T2> | O<T3>
. Mas agora isso não acontece. Ou tal chamada de função é rejeitada imediatamente ou é aceita, mas produz uma saída do tipoO<T1 | T2 | T3>
. Há uma solicitação de recurso em microsoft/TypeScript#52295 para distribuir automaticamente a aplicação de função sobre argumentos do tipo union, mas não é um recurso muito procurado e implementá-lo degradaria significativamente o desempenho do compilador. Então, a menos que uma função genérica seja explicitamente escrita para distribuir entre uniões em suas entradas, isso não acontecerá.As tipagens da biblioteca TypeScript se
Array.prototype.filter()
parecem com estas:Esse é um método sobrecarregado com múltiplas assinaturas de chamada. A primeira assinatura de chamada lida com a situação em que o
predicate
age como uma função de proteção de tipo e permite que o tipo de array de saída seja mais estreito do que a entrada that (portanto,["a", "b", 1, 2, "c"].filter(x => typeof x === "string")
é do tipostring[]
and not(string | number)[]
). A segunda assinatura de chamada é o caso usual em que o predicado não pode ser usado para restringir os tipos de elementos.Nenhuma dessas assinaturas tenta distribuir sobre uniões no array no qual o método é chamado. Se você quiser um comportamento como esse, precisará escrevê-lo explicitamente você mesmo.
Se você quiser que o comportamento union-distribution aconteça automaticamente para todos os arrays na sua base de código, você pode mesclar novas assinaturas de chamada de sobrecarga na
Array<T>
interface global (e talvezReadonlyArray<T>
também). Talvez assim:Aqui estamos adicionando um novo parâmetro de tipo genérico
A
aofilter()
método, representando o tipo do array no qual você o chama. Para inferir esse tipo corretamente, o método tem umthis
parâmetro do tipoA
. Umthis
parâmetro é um parâmetro fingido que não existe em tempo de execução; ele apenas representa o tipo do objeto no qual o método é chamado. Então, quando você chamamyList.filter(⋯)
, deve ser queA
é inferido comotypeof myList
.Então há muito malabarismo de tipos. O tipo
A extends Array<infer T> ? ⋯ : never;
é um tipo condicional distributivo que produz explicitamente uma união de⋯
saídas quandoA
é uma entrada de união. Para o caso de proteção de tipo, temos que tentar estreitar cada membro da união de uma matriz de seu tipo de elemento original para um que também seja atribuível ao tipo protegido por tipoS
, daí a interseção . Para o caso normal, ainda temos que distribuir sobre o tipo de elemento regular. Você pode se perguntar por que não retornamos apenasA
. Isso porqueA
pode ser um tipo de tupla como[string, number, boolean]
, e não queremos retornarfilter()
esse mesmo tipo. Um tipo como(string | number | boolean)[]
é mais apropriado.Isso resulta no seguinte comportamento:
Isso funciona muito bem, mas pode haver alguns casos extremos bizarros que eu não previ. Se você não quiser aplicar isso a toda a sua base de código, você pode querer apenas escrever uma
filter()
função com essas assinaturas de chamada como um wrapper:Isso é um pouco mais simples porque não precisamos brincar com
this
parâmetros e inferência. O TypeScript pode inferirA
diretamente doarr
parâmetro real. O comportamento é o mesmo:Mas agora você não precisa se preocupar com o
filter()
comportamento estranho do método de array nativo em alguma parte remota da sua base de código.Mas mesmo isso é um pouco complicado e ainda pode ter casos extremos. Se você vai fazer muita filtragem de uniões de arrays, então posso ver que vale a pena. Mas se você só vai fazer isso uma ou duas vezes na sua base de código, você provavelmente deve usar uma asserção de tipo e seguir em frente:
É de longe a abordagem mais simples e, embora não seja verificável quanto ao tipo, a complexidade das outras abordagens é provavelmente mais propensa a erros. Quero dizer, por um lado, você está dizendo ao TypeScript que
filtered0
é umstring[] | number[]
, e, por outro lado, você está tentando dizer ao TypeScript que toda chamada parafilter()
faz essa coisa complicada com uniões no nível do tipo. Se você não está muito confiante de que a coisa complicada está correta, então provavelmente é mais seguro apenas verificar suas asserções.Link do playground para o código
Infelizmente, algumas definições de métodos JS/DOM padrão são tipadas de forma mais ampla do que o esperado. Você poderia aumentar
Array
assim:Parque infantil