Introdução
Dentre as diversas ferramentas de tipagem avançada que o TypeScript provê, tipos condicionais são uma das mais importantes, pois nos dá habilidade de criar tipos não uniformes, ou seja, que podem variar conforme a entrada.
Sua sintaxe é análoga a do operador ternário do TypeScript, sendo que, a condição deve expressar um teste de relação entre os tipos.
Exemplo lado a lado:
/*
* A condição pode ser qualquer tipo de operação que
* resulte em um boleano.
*/
const ternary = condition ? expr1 : expr2;
/*
* A condição deve ser criada testando a relação entre
* os dois tipos, sempre utilizando o operador `extends`.
*/
type Conditional<T, U> = T extends U ? string : number;
Condições simples
type IsString<T> = T extends string ? true : false;
type R1 = IsString<'Hello'>;
// => true
type R2 = IsString<2>;
// => false
No exemplo acima, criamos o tipo IsString
que verifica se T
é atribuível a string
, retornando true
caso verdadeiro, ou false
caso contrário.
Agora, observe atentamente o exemplo abaixo:
type Is2020<T> = T extends true ? 'yes' : 'no';
const year = new Date().getFullYear() === 2020;
type R = Is2020<typeof year>;
// => 'yes' | 'no'
Indo direto para a última linha do exemplo, note que Is2020<typeof year>
está retornando como resultado uma união entre os dois tipos possíveis especificados na sua assinatura, mas por quê? Para responder essa pergunta, temos que ter em mente que o TypeScript roda em tempo de desenvolvimento e não consegue inferir tipos de runtime, que no nosso caso, é o trecho new Date().getFullYear() === 2020
. Como inferir não foi possível, recebemos como resultado uma união com todas as possibilidades.
Condições encadeadas
Assim como um ternário comum, podemos encadear tipos condicionais e criar tipagens mais robustas baseadas em múltiplas regras.
type IsFrontEndFramework<T> = T extends string
? T extends 'Angular'
? true
: T extends 'Vue'
? true
: T extends 'React'
? true
: 'Insert a valid framework.'
: never;
type R1 = IsFrontEndFramework<'React'>;
// => true
type R2 = IsFrontEndFramework<'Hello'>;
// => 'Insert a valid framework.'
type R3 = IsFrontEndFramework<920>;
// => never
Infer
indo Tipos
Até agora vimos que é possível encadear e criar condições variadas que cobrem uma infinidade de cenários, porém, em certos casos, ainda é muito trabalhoso ou praticamente impossível criar a constraint correta apenas utilizando condições simples. Para esses cenários mais complexos, temos a keyword infer
, que auxilia na inferência de tipos dinâmicos dentro das nossas condições. A única limitação aqui é: você só poderá utilizar infer
numa condição extends
.
Array
Vamos observar o exemplo a seguir, onde utilizamos infer
para inferir o tipo de um array:
type InferArrayType<A> = A extends Array<infer U> ? U : never;
type R1 = InferArrayType<Array<string>>;
// => string
type R2 = InferArrayType<Array<number>>;
// => number
type R3 = InferArrayType<Array<Array<string>>>;
// => string[]
Atenção no InferArrayType
, um tipo que recebe um genérico A
, valida se este tipo é um array e, caso seja, retorna o tipo do elemento do array. Note que o tipo U
é inferido dinamicamente utilizando a keyword infer
.
Esse é um exemplo simples, mas que demonstra a potência do infer
em situações mais complexas.
Agora, vamos ver um exemplo mais complexo, onde utilizamos infer
para converter um objeto de duas propriedades em uma tupla:
type ObjectToTuple<T> = T extends { a: infer A; b: infer B } ? [A, B] : never;
type Result = ObjectToTuple<{a: string; b: number}>;
// => [string, number]
No exemplo acima, ObjectToTuple
é um tipo que recebe um genérico T
, valida se T
é um objeto com as propriedades a
e b
, e retorna um array com os tipos das propriedades a
e b
. Note que A
e B
são inferidos dinamicamente utilizando a keyword infer
.
Inferindo parâmetros ou retorno de funções
A versatilidade do infer
vai além da inferência de tipos simples, também sendo possível inferir tipos de funções. No exemplo abaixo, iremos inferir o tipo do parâmetro de uma função de aridade 1 e o tipo de retorno da função.
type FunctionArg<F> = F extends (p: infer P) => unknown ? P : never;
type R1 = FunctionArg<(x: number) => boolean>;
// => number
type R2 = FunctionArg<(x: number, y: number) => boolean>;
// => never
Neste exemplo, a função FunctionArg
recebe um tipo genérico F
e verifica se F
é uma função de aridade 1. Caso seja, o tipo do parâmetro é inferido e retornado. Caso a aridade seja maior que 1, o tipo never
é retornado.
Mas e se quisermos inferir os tipos de uma função com aridade maior que 1? Podemos fazer isso com a seguinte implementação:
type FunctionArg<F> = F extends (...args: infer P) => unknown ? P : never;
type R1 = FunctionArg<(x: number) => boolean>;
// => [number]
type R2 = FunctionArg<(x: number, y: number) => boolean>;
// => [number, number]
Neste caso, a função FunctionArg
recebe um tipo genérico F
e verifica se F
é uma função de aridade n
. Caso seja, o tipo dos parâmetros é inferido e retornado em um array.
Para inferir o tipo de retorno de uma função, podemos fazer o seguinte:
type FunctionReturn<F> = F extends (...args: any[]) => infer R ? R : never;
type R1 = FunctionReturn<(x: number) => boolean>;
// => boolean
type R2 = FunctionReturn<(x: number, y: number) => boolean>;
// => boolean
Neste caso, a função FunctionReturn
recebe um tipo genérico F
e verifica se F
é uma função de aridade n
. Caso seja, o tipo de retorno é inferido e retornado.
Importante
- Apesar de ser uma ferramenta poderosa, tipos condicionais podem ser complexos e difíceis de entender, especialmente quando combinados com
infer
. - É de extrema importância avaliar as necessidades de cada caso para decidir se é ou não necessário.
- Tipos condicionais mal escritos podem piorar a experiência de desenvolvimento e tornar o código mais difícil de entender. Use com moderação e sempre busque a simplicidade.
Conclusão
Tipos condicionais são uma ferramenta poderosa para criar tipos não uniformes e mais robustos. A keyword infer
é uma ferramenta essencial para inferir tipos dinâmicos dentro de condições. Combinando essas duas ferramentas, podemos criar tipos complexos e expressivos que cobrem uma grande variedade de cenários e que aumentam a segurança e a robustez do nosso código.