Narrowing
Narrowing em typescript é uma forma de garantir que o tipo de uma variável é o que você espera. Por exemplo, se você tem uma variável que pode ser string ou number, você pode usar o operador typeof para garantir que a variável é do tipo que você espera.
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input; // (parameter) padding: number
}
return padding + input; // (parameter) padding: string
}No TypeScript, a narrowing refere-se ao processo de restringir o tipo de uma variável ou expressão de um tipo mais amplo para um tipo mais específico.
O TypeScript usa tipos condicionais e proteções de tipo para atingir o estreitamento. Uma proteção de tipo é uma maneira de verificar o tipo de um valor em tempo de execução, permitindo que o compilador infira um tipo mais específico para esse valor. Isso pode ser útil em situações em que você precisa executar diferentes operações com base no tipo de uma variável ou expressão.
Por exemplo, considere o seguinte código TypeScript:
let myVar: string | number = "hello";
if (typeof myVar === "string") {
console.log(myVar.toUpperCase()); // TypeScript knows that myVar is a string
console.log(myVar.toFixed(2)); // TypeScript knows that myVar is a number
}Typeof type guards
- "string"
- "number"
- "bigint"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"
Truthiness narrowing
A função abaixo produz um erro, pois o compilador não consegue garantir que strs não é null. Mesmo que existe o type guard typeof strs === "object", o compilador não consegue garantir que strs não é null dentro do bloco if, pois typeof null é na verdade "object"! Este é um daqueles infelizes acidentes da história.
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) { // Object is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}Ok! Podemos usar condições coercitivas para garantir que strs não é null dentro do bloco if. Utilizamos os operadores && e || para garantir que strs não é null ou undefined dentro do bloco if.
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}Equality narrowing
O TypeScript também usa instruções switch e verificações de igualdade como ===, !==, == e != para restringir os tipos. Por exemplo. Quando verificamos que x e y são iguais no exemplo abaixo, o TypeScript sabe que seus tipos também tem que ser iguais.
function example(x: string | number, y: string | boolean) {
if (x === y) {
x.toUpperCase();
y.toLowerCase();
} else {
console.log(x);
}
}No seguinte exemplo, strs como '' não é tratada pois o Javascript identifica como falso e não não cai no bloco else.
function printAll(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
console.log(printAll('')) // undefinedPara resolver isso, fazemos a comparação not-null para validar que '' é uma string.
function printAll(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs); // [empty string]
}
}
}
console.log(printAll('')) // undefinedThe in operator narrowing
O operador in é usado para verificar se uma propriedade existe em um objeto. O exemplo abaixo verifica se a propriedade name existe no objeto person.
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}The instanceof operator narrowing
O JavaScript possui um operador para verificar se um valor é ou não uma “instância” de outro valor. Mais especificamente, em JavaScript x instanceof Foo verifica se a cadeia de protótipos de x contém Foo.prototype
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}Assignments
TypeScript examina o lado direito da atribuição e restringe o lado esquerdo adequadamente.
let x = Math.random() < 0.5 ? 10 : "hello world!";
// x: string | number
x = "hello world!";
x = 10;
x = true; // Error because boolean isn't (string | number)Control flow analysis
O TypeScript usa análise de fluxo de controle para determinar o tipo de uma variável. Em diferentes ramificações o tipo da variável pode ser diferente. Por exemplo, no exemplo abaixo, o tipo de x é number dentro do bloco if e number | undefined fora do bloco if.
function f(x?: number) {
if (x) {
// x: number
}
// x: number | undefined
}Type predicates
Por vezes você deseja um controle mais direto sobre como os tipos mudam em todo o código. Para definir um tipo de proteção definido pelo usuário, basta definir uma função cujo tipo de retorno seja um predicado.
Sempre que isFish for chamado com alguma variável, o TypeScript restringirá essa variável a esse tipo específico se o tipo original for compatível.
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}Discriminated unions
Para alguma motivação, vamos imaginar que estamos tentando codificar formas como círculos e quadrados. Os círculos acompanham seus raios e os quadrados acompanham os comprimentos de seus lados. Usaremos um campo chamado kind para dizer com qual forma estamos lidando. Aqui está uma primeira tentativa de definir Forma.
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
// Like a radius is optional, shape.radius is optional too
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
// Object is possibly 'undefined'.
}
// We can use non-null assertion operator to avoid error
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius! ** 2;
}
}Now, we divide the Shape interface into two interfaces, Circle and Square. The Shape interface is a union of the two. This is called a discriminated union.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
// Now, shape.radius is not optional
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
}The never type
O tipo never representa o tipo de valores que nunca ocorrem. Por exemplo, never é o tipo de retorno para uma função que sempre lança uma exceção ou uma função que nunca retorna; as variáveis do tipo never só podem ser never por designação.
Ao estreitar, você pode reduzir as opções de uma união a um ponto em que removeu todas as possibilidades e não sobrou nada. Nesses casos, o TypeScript usará um tipo never para representar um estado que não deveria existir.
Exhaustiveness checking
O tipo never pode ser atribuído a todos os tipos; no entanto, nenhum tipo pode ser atribuído a never (exceto o próprio never). Isso significa que você pode usar o narrowing e confiar em nunca aparecer para fazer uma verificação exaustiva em uma instrução switch.
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}