O que é Type Narrowing e como ele funciona?
Haverá momentos em que você terá um valor com um tipo amplo e precisará restringi-lo para um tipo mais específico. Por exemplo, talvez você precise garantir que um objeto corresponda a uma interface que você definiu. Ou uma string está dentro de uma lista específica de valores. Existem várias maneiras de conseguir isso.
O primeiro é o estreitamento por veracidade. Considere nosso exemplo da última lição:
const email = document.querySelector<HTMLInputElement>("#email");
console.log(email.value);
Recebemos um erro de compilador ao tentar acessar a propriedade value de email, porque email pode ser null. No entanto, podemos usar uma declaração condicional para confirmar que email é truthy antes de acessar a propriedade:
const email = document.querySelector<HTMLInputElement>("#email");
if (email) {
console.log(email.value);
}
Neste exemplo atualizado, porque null não é um valor truthy, o TypeScript consegue inferir que email DEVE ser um elemento input dentro do bloco condicional. Para que ele não gere mais um erro de compilação.
Verificações de veracidade também podem funcionar na direção oposta:
const email = document.querySelector<HTMLInputElement>("#email");
if (!email) {
throw new ReferenceError("Could not find email element!")
}
console.log(email.value);
Com essa abordagem, lançamos um erro se email for falsy. null é um valor falsy. Lançar um erro termina a execução lógica deste código, o que significa que quando chegamos à chamada console.log() o TypeScript sabe que email não pode ser null.
Encadeamento opcional também é uma forma de restrição de tipo, sob a mesma premissa de que o acesso à propriedade não pode acontecer se o valor de email for null.
const email = document.querySelector<HTMLInputElement>("#email");
console.log(email?.value);
Mas e quanto a outros tipos? Bem, você também pode restringir tipos usando o operador typeof. Vamos ver um exemplo de uma variável que indicamos que pode ser uma string OU um número:
const myVal = Math.random() > 0.5 ? 222 : "222";
console.log(myVal / 10)
Neste exemplo, vemos um erro de compilador porque não podemos realizar operações aritméticas em um valor do tipo string. Mas podemos usar uma condicional para verificar o typeof da variável myVal:
const myVal = Math.random() > 0.5 ? 222 : "222";
if (typeof myVal === "number") {
console.log(myVal / 10);
}
Porque usamos a palavra-chave typeof, o TypeScript agora sabe que myVal tem que ser um número e podemos realizar operações aritméticas com segurança.
Mas e quanto a tipos de objetos mais complexos? Se o objeto em questão vier de uma classe, você pode realmente usar a palavra-chave instanceof para restringir o tipo. Voltando ao nosso exemplo de querySelector():
const email = document.querySelector("#email");
Ao invés de passar um tipo genérico e dizer ao TypeScript qual é o elemento, podemos usar instanceof para restringir o tipo e escrever um código mais seguro:
const email = document.querySelector("#email");
if (email instanceof HTMLInputElement) {
console.log(email.value);
}
Essa abordagem pode parecer a mesma da nossa anterior, mas instanceof é uma validação em tempo de execução - o que significa que, se de alguma forma errarmos o tipo TypeScript, nosso código JavaScript ainda confirmará que email é um elemento input.
A seguir, vamos ver um exemplo onde buscamos um objeto User de uma API e tentamos imprimir as informações:
interface User {
name: string;
age: number;
}
const printAge = (user: User) =>
console.log(${user.name} is ${user.age} years old!)
const request = await fetch("url")
const myUser = await request.json();
printAge(myUser);
Teremos um erro de compilação ao tentar passar myUser para a função porque, mesmo sabendo que a API retorna o objeto correto, o TypeScript não reconhece. E o método .json() não aceita um tipo genérico.
A maneira "fácil" de resolver esse problema seria fazer o cast do tipo:
interface User {
name: string;
age: number;
}
const printAge = (user: User) =>
console.log(${user.name} is ${user.age} years old!)
const request = await fetch("url")
const myUser = await request.json() as User;
printAge(myUser);
Mas sempre que você faz um cast do tipo, você está essencialmente enfraquecendo a capacidade do TypeScript de detectar erros potenciais. Então, ao invés de fazer o cast do tipo, você pode escrever um type guard:
interface User {
name: string;
age: number;
}
const isValidUser = (user: unknown): user is User => {
return !!user &&
typeof user === "object" &&
"name" in user &&
"age" in user;
}
O tipo de retorno aqui é o componente chave desta definição de função. A sintaxe user is User indica que nossa função retorna um valor booleano que, quando true, significa que o valor user satisfaz a interface User. Em seguida, fazemos algumas verificações básicas para garantir que a estrutura do objeto user corresponda - observe o uso de uma restrição de veracidade (!!user) e uma restrição de typeof. Devemos fazer isso porque typeof null retorna "object":
interface User {
name: string;
age: number;
}
const isValidUser = (user: unknown): user is User => {
return !!user &&
typeof user === "object" &&
"name" in user &&
"age" in user;
}
const printAge = (user: User) =>
console.log(${user.name} is ${user.age} years old!)
const request = await fetch("url")
const myUser = await request.json() as User;
if (isValidUser(myUser)) {
printAge(myUser);
}
Agora, se combinarmos toda a nossa lógica, não teremos mais erros de compilação e poderemos construir nosso código com sucesso.
A restrição de tipo é um recurso poderoso que ajuda você a escrever código mais seguro e com menos erros - mas lembre-se de que os tipos do TypeScript não são completamente rígidos, então evite práticas como fazer cast do tipo de um valor sem restringi-lo.Este módulo não possui perguntas. Marque como concluído.