
Continuando nossa jornada pela série de postagens inspiradas no livro "Effective TypeScript", exploraremos agora o capítulo 3.
Até o momento, discutimos alguns tópicos dos capítulos 1 e 2, como declarações de tipo em vez de asserções, excesso de verificação de propriedades, tipos em declarações de funções e a diferença entre type e interface.
Se ainda não conferiu a primeira postagem, clique aqui para ler.
Usando variáveis separadas para tipos diferentes
No JavaScript, reutilizar uma variável para armazenar valores de tipos e propósitos diferentes não é um problema. Porém, em TypeScript, isso pode gerar erros:
let id = "12-34-56";
fetchProduct(id);
id = 123456; // Type 'number' is not assignable to type 'string'
fetchProductBySerialNumber(id); // Argument of type 'string' is not assignable to parameter of type 'number'
Para corrigir esses erros e garantir que o tipo da variável não mude, podemos usar uma abordagem de tipo de união:
let id: string | number = "12-34-56";
fetchProduct(id);
id = 123456; // Tudo ok agora!
fetchProductBySerialNumber(id);
No entanto, embora um tipo de união funcione, ele pode criar mais problemas no futuro. Os tipos de união são mais difíceis de trabalhar do que tipos simples como string ou number, porque você geralmente precisa verificar quais são eles antes de fazer qualquer coisa.
A abordagem mais clara é criar variáveis separadas:
const id = "12-34-56";
fetchProduct(id);
const serial = 123456; // Tudo ok agora!
fetchProductBySerialNumber(serial);
Essa abordagem oferece vantagens como separação de responsabilidades, melhor inferência de tipos e facilita a leitura do código.
O que é Type Widening?
Em tempo de execução, uma variável possui um único valor. Mas durante a análise estática, o TypeScript atribui um conjunto de valores possíveis ao tipo da variável — esse processo é conhecido como widening (ou "alargamento").
Para controlar esse comportamento, podemos utilizar const para declarar variáveis, proporcionando um tipo mais restrito.
Vamos ver um exemplo: você escreve um tipo para um vetor 3D e uma função para obter o valor de qualquer um de seus componentes:
interface Vector3 {
x: number
y: number
z: number
}
function getComponent(vector: Vector3, axis: "x" | "y" | "z") {
return vector[axis]
}
Mas quando você vai usar, o TypeScript sinaliza um erro:
let x = "x"
let vec = { x: 10, y: 20, z: 30 }
getComponent(vec, x)
// Argument of type 'string' is not assignable to parameter of type '"x" | "y" | "z"'
O código funcionaria em tempo de execução, mas por que o erro? O tipo de x foi inferido como string (amplo), enquanto getComponent esperava o tipo literal "x" | "y" | "z" (restrito).
Declarar a variável com const em vez de let resolve o problema, pois o TypeScript infere o tipo literal diretamente:
const x = "x"; // Type is 'x'
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); // Tudo ok agora!
O que é Type Narrowing?
O narrowing é o processo inverso: o TypeScript passa de um tipo amplo para um tipo mais restrito. É o oposto do widening.
const el = document.getElementById("foo"); // Type is HTMLElement | null
if (el) {
el.innerHTML = "Party Time".blink(); // Type is HTMLElement
} else {
alert("No element #foo"); // Type is null
}
Isso ocorre frequentemente em condicionais, como demonstrado no exemplo de verificação de null. O verificador de tipos é eficaz em restringir tipos dentro de blocos condicionais, proporcionando maior facilidade de manipulação e mais segurança em tempo de execução.