Estou escrevendo uma função que aceita uma tupla e retorna uma tupla do mesmo tipo. A tupla de entrada pode ter alguns comprimentos diferentes, e quero que a função expresse que a tupla retornada tem o mesmo comprimento; então estou usando um genérico para digitar a entrada e a saída. No entanto, o corpo da função precisa modificar o primeiro elemento da tupla separadamente do restante dos elementos da tupla, então tenho que desestruturar e remontar a tupla. Aqui está um exemplo mínimo do que não consigo fazer com que o TypeScript aceite:
type B = [string, string] | [string, string, string];
function foo<T extends B>([hd, ...tl]: T): T {
return [hd, ...tl]
}
Isso me dá o seguinte erro:
Type '[string, string] | [string, string, string]' is not assignable to type 'T'.
'[string, string] | [string, string, string]' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'B'.
Type '[string, string]' is not assignable to type 'T'.
'[string, string]' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'B'.(2322)
Acredito que o que está acontecendo é que o TypeScript infere um tipo excessivamente geral para a tl
variável. O tl
tipo da variável deve refletir que é a cauda de type T
, mas, em vez disso, quando passo o mouse sobre tl
ela, mostra que ela é inferida como o tipo mais geral [string] | [string, string]
. Como tl
o tipo inferido de "esquece" sua relação com o T
parâmetro de tipo, o TypeScript pensa incorretamente que é possível T
e [hd, ...tl]
ser diferentes subtipos de B
.
Aqui está outro exemplo semelhante de TypeScript inferindo tipos excessivamente gerais que "esquecem" sua conexão com o parâmetro de tipo genérico:
type B = [1, 2] | [3, 4];
function foo<T extends B>([x, y]: T): T {
return [x, y]
}
Aqui pensa que o valor de retorno [x, y]
é do tipo [1 | 3, 2 | 4]
, mesmo sendo impossível que seja igual, por exemplo, [1, 4]
.
Como posso fazer com que o TypeScript reconheça que a remontagem de um genérico desestruturado é do mesmo tipo que o genérico?
De modo geral, a única maneira de o TypeScript ter certeza de que você retornará um valor de um tipo genérico
T
é retornar um valor que ele já sabe que é desse tipo. Você pode ter alguma invariante em mente sobre um tipo genérico, como "para todos arraylikeT
, se eu dividirT
em pedaços e depois remontar os pedaços em um array, eu voltareiT
novamente", mas o TypeScript não entende essas coisas. O raciocínio de ordem superior sobre genéricos precisa ser explicitamente programado na linguagem para circunstâncias específicas; e isso só acontece em casos comuns em que o benefício de fazer uma verificação extra de tipo vale o custo. Além disso, tais invariantes tendem a não ser verdadeiros:Aqui aconteceu o problema exato sobre o qual o compilador estava avisando. Quando liguei
foo()
, passei um valor do tipo[string, string] & {prop: string}
, que é o queT
é instanciado. Em seguida, retornamos um valor que o TypeScript sabe que pode ser atribuído aB
, mas não sabe se pode ser atribuído aT
. E não é. O tipo deoutput
é considerado igual ainput
e, portanto, deve ter uma propriedadestring
com valor .prop
Isso não acontece, causando um erro em tempo de execução.Mesmo que você não ache que isso seja provável, você disse que está planejando modificar o primeiro elemento do array. Novamente, é muito difícil ter certeza de que qualquer coisa que você fizer para modificar um valor desse tipo será atribuível posteriormente a esse mesmo tipo, se esse tipo for genérico. Os genéricos realmente restringem as coisas de uma forma que os tipos específicos não fazem. Só porque uma função transforma
string
s emstring
s enumber
s emnumber
s eDate
s emDate
s não significa que ela se torne genéricaT extends string | number | Date
emT
.T
pode ser uma string ou um tipo literal numérico . Ou pode ser umDate
com propriedades extras. Modificação de valores e genéricos são inerentemente incompatíveis.A coisa mais fácil a fazer é dizer "Não acho que isso seja algo com que eu deva me preocupar" e apenas usar um tipo assertion :
Se atender às suas necessidades e o seu modelo mental de
T
-torna-se-T
se mantiver na prática porque ninguém se preocupa em investigar os casos patológicos, ótimo. Mas não é realmente uma falha do TypeScript aqui que você precise usar asserções de tipo.Por outro lado, se você quiser alguma segurança de tipo verificada pelo compilador, você deve abandonar a ideia de
T
-in-and-T
-out e, em vez disso, impor alguma estrutura específica nos tipos de entrada e saída. Sua função ainda pode ser genérica, mas só deve ser genérica nas partes em que você transfere valores sem modificá-los. Por exemplo:Isso pode ser um exagero, mas vamos dar uma olhada. Primeiro, esqueça as restrições complicadas e imagine que escrevemos
A função possui dois parâmetros de tipo genérico;
H
para a cabeça eT
para a cauda da matriz de entrada. Usamos tipos de tuplas variáveis para representar a entrada e a saída. TypeScript é capaz de verificar isso[hd, ...tl]
e[H, ...T]
combinar entre si. Não há erro do compilador.As restrições apenas forçam você a passar apenas coisas atribuíveis a
B
.H
deve ser atribuível aoB[0]
tipo de acesso indexado correspondente ao primeiro elemento deB
. Depois de escolherH
, entãoT
deve ser atribuível aRestMatching<B, H>
, o que significa que é o restante dos membrosB
cujo primeiro elemento éH
. Esse tipo condicional distributivoRestMatching<B, H>
se divideB
em seus membros do sindicato, verifica cada um deles para começar com algo atribuível aH
e, para qualquer um deles que comece com algoH
compatível, reunimos a cauda desse membro do sindicato.Você pode verificar se isso funciona:
Observe que o texto acima é apenas uma das muitas maneiras de fazer com que as entradas
hd
etl
dependam uma da outra da maneira adequada. É bastante geral para arbitrárioB
. Mas se você tiver um tipo mais estruturadoB
(como, cada primeiro elemento é um tipo literal de string, como["add", number, number] | ["negate", number]
por exemplo), poderá escrevê-lo de maneiras menos complicadas (usando um tipo como,{add: [number, number]; negate: [number]}
por exemplo). Não vou entrar nisso aqui.A parte importante aqui é que nunca tentamos afirmar ou afirmar que uma coisa modificada é do mesmo tipo genérico que a coisa original. Qualquer coisa que modificamos é de um tipo específico e não genérico.
Link do Playground para o código