Neste exemplo:
type Action<T extends string, P = never> = {
type: T;
} & ([P] extends [never]
? {}
: {
payload: P;
});
Por que precisamos de colchetes em torno do condicional [P] extends [never]
e qual é a diferença se não o usarmos assim P extends never
?
Entendo o que esse tipo está fazendo, mas não consigo encontrar uma explicação sobre por que realmente precisamos de colchetes e por que não funciona sem eles.
EDIT: Deixe-me tentar com um exemplo mais simples:
type GetTest<T = never> = [T] extends [never] ? string : number;
type Test = GetTest; // type Test = string;
Aqui está type Test = string
o esperado. Mas o que não entendi é o que acontece com a condição quando não temos colchetes, não obtemos, string
mas também não obtemos um number
, em vez disso, obtemostype Test = never
type GetTest<T = never> = T extends never ? string : number;
type Test = GetTest; // type Test = never
Se você tiver um tipo condicional em que o tipo que está sendo verificado (à esquerda de
extends
) é um parâmetro de tipo genérico , ele se torna um tipo condicional distributivo . Então emGetTestDist<T>
é um tipo condicional distributivo porqueT
é um parâmetro de tipo genérico, enquantoGetTestNonDist<T>
não é porque[T]
não é um parâmetro de tipo genérico.Sim,
[T]
envolve o parâmetro de tipo genéricoT
, mas não é um parâmetro de tipo em si. Às vezes, as pessoas dizem que oT
inT extends never ? ⋯ : ⋯
é um parâmetro de tipo nu ou um parâmetro de tipo nu , em oposição aoT
in[T] extends [never] ? ⋯ : ⋯
, que é, digamos, "vestido".Portanto, um tipo condicional em que o tipo que está sendo verificado é um parâmetro de tipo nu é um tipo condicional distributivo. Mas o que isso significa?
Quando uma função de tipo
F<T>
é um tipo condicional distributivo, a operação distribui sobre as uniões emT
, o que significa que ela atua em cada membro da união individualmente e junta o resultado novamente em uma união. Então, por exemplo,F<A | B | C>
será avaliado como o mesmo queF<A> | F<B> | F<C>
.Além disso, o
never
tipo é considerado a união vazia , portanto,F<never>
sempre será,never
não importa o que aconteça. (consulte o comentário relevante em microsoft/TypeScript#23182 para obter uma fonte confiável).Este tratamento
never
pode ser confuso, mas é consistente. Onever
tipo é absorvido pelas uniões: isto é,X | never
é reduzido aX
não importa o queX
seja. (Se você tiver um valorx
do tipo ,X | never
entãox
é do tipoX
ou do tiponever
, mas não há valores do tiponever
, portanto,x
deve ser do tipoX
. Portanto, todos os valores atribuíveis aX | never
devem ser atribuíveis aX
, e o verificador de tipo observa isso absorvendonever
em unions. Consulte Por que nunca é atribuível a todos os tipos? para obter mais informações.) Você pode pensar nissonever
como o elemento de identidade da operação de união.Portanto, se
X | never
é equivalente anever
, entãoF<X>
eF<X | never>
são iguais. SeF<T>
é distributivo, entãoF<X | never>
é equivalente aF<X> | F<never>
. Isso implicaF<X>
é o mesmo queF<X> | F<never>
para todosX
. A maneira mais fácil de isso ser verdade é ifF<never>
is justnever
.(Por analogia, imagine que você tem uma função matemática ?(?) que é distributiva sobre a adição , de modo que ?(?) + ?(?) = ?(?+?). A adição tem um elemento de identidade: 0, porque ?+ 0 = ?. Isso implica que ?(0) deve ser 0, porque ?(?) + ?(0) = ?(?+0) = ?(?). 0 é "a soma vazia" e é "a soma
never
vazia União".)Muitas vezes, o comportamento distributivo é desejado, mas às vezes não é. A maneira de desativar esse comportamento é, portanto, "vestir" o parâmetro de tipo nu e o tipo que você está verificando com alguma função de tipo covariante
C<T>
. (Consulte Diferença entre variação, covariância, contravariância e bivariância no TypeScript para obter mais informações sobre variação).Você precisa fazer isso em ambos os lados
extends
de modo que esteja fazendo a mesma verificação (seC<T>
for covariante emT
entãoC<A> extends C<B>
implicaA extends B
). Qualquer "roupa" covariante será suficiente aqui.Mas se você está procurando as roupas "mais curtas" para economizar o máximo de pressionamentos de tecla possível, use
[T]
. Para o bem ou para o mal, o TypeScript considera os tipos de matriz como covariantes (consulte Por que as matrizes do TypeScript são covariantes? ), e uma tupla de um elemento é um tipo de matriz. Não há nada mais "correto" sobre o uso[T]
de qualquer outra função de tipo covariante, é apenas o mais conveniente.E, portanto, geralmente é recomendado que, se você quiser desativar a distribuição sobre uniões em um tipo condicional, envolva os dois lados do
extends
com[
⋯]
: