Narrowing

Narrowing

No Typescript, nós temos um recurso dedicado a identificar alguns tipos de dados, com o objetivo de indicar uma direção diferente a execução do nosso programa.

Essa indicação é baseada no tipo de dado que foi identificado durante uma verificação condicional, isto é, um if e else da vida que analisa o tipo de dado de modo a seguir caminhos diferentes dentro da nossa aplicação.

Na lição que falamos sobre Union Type, nós vimos que uma variável ou uma função, pode receber mais de um tipo específico de dado, como uma string ou um number, por exemplo:

let valor: string | number = "Micilini Roll";

valor = 98;
valor = "https://micilini.com/"

Tal recurso que permite que uma variável seja de um ou mais tipos diferentes, graças a um recurso conhecido como Union Type!

Com isso em mente, vamos analisar mais a fundo o que vem a ser o narrowing 🙂

O que é Narrowing?

Narrowing é um processo pelo qual o Typescript determinada e refina o tipo de variável que está sendo apresentada em questão para um tipo mais específico.

Isto é, com base nas verificações condicionais ou outras construções de códigos que podemos usar durante essa validação.

Tal processo oferece melhores garantias de identificação de tipos, o que ajuda na identificação de possíveis erros que podem acontecer durante a nossa aplicação.

Agora deixando toda a teoria de lado, o narrowing nada mais é do que a gente pegar um Union Type, e por meio de uma verificação condicional (if...else) verificar se o valor armazenado é uma string, ou um number, ou um boolean, ou um objeto e etc... usando um typeof ou um instanceof.

Vejamos alguns exemplos:

function processar(valor: string | number) {
 if (typeof valor === "string") {
 // Narrowing: Aqui, o TypeScript sabe que `valor` é uma `string`
 console.log(valor.toUpperCase());
 } else {
 // Narrowing: Aqui, o TypeScript sabe que `valor` é um `number`
 console.log(valor.toFixed(2));
 }
}

No caso do exemplo acima, nós temos uma função chamada de processar que recebe por parâmetro uma variável (valor) que pode ser tanto do tipo string, quanto do tipo number.

Onde verificamos por meio de uma condicional (if...else) se o tipo de dado armazenado dentro da variável (valor) é uma string ou qualquer outro valor (que no caso o TS já sabe que é um number).

No exemplo acima, o uso de typeof permite que o TypeScript "reduza" o tipo da variável valor para string ou number dependendo do resultado da verificação.

Vejamos um outro exemplo usando o instanceof:

class Animal {
 nome: string;
 constructor(nome: string) {
 this.nome = nome;
 }
}

class Cachorro extends Animal {
 latir() {
 console.log("Au au!");
 }
}

function fazerBarulho(animal: Animal) {
 if (animal instanceof Cachorro) {
 // Aqui TypeScript sabe que animal é um Cachorro
 animal.latir();
 } else {
 // Aqui animal é apenas um Animal
 console.log("Não é um cachorro!");
 }
}

No caso do exemplo acima, nós temos uma função chamada de fazerBarulho, que inicialmente recebe por parâmetro uma interface, que como vimos em lições anteriores, pode estar representada (ligada) a uma classe/função Cachorro ou qualquer outro animal.

Se o TS identificar que a variável (animal) está armazenando uma instância da classe Cachorro, ele executa o método latir().

Caso contrário, ele mostra uma simples mensagem no console alegando que o valor armazenado dentro da variavel (animal) não é um cachorro!

O instanceof permite verificar se um objeto é uma instância de uma classe específica, permitindo ao TypeScript saber qual tipo específico ele é.

No código acima, usamos o instanceof em conjunto com as interfaces que aprendemos em lições anteriores, agora vamos ver um exemplo mais simples que verifica apenas os nomes das classes:

class Usuario{
 nome = "Micilini";
}

class Rank{
 rank = 99;
}

function processarClasses(classe: object){
 if(classe instanceof Usuario){
 //Trato a classe como se fosse um Usuario
 }else if(classe instanceof Rank){
 //Trato a classe como se fosse um Rank
 }else{
 //Faço qualquer outra coisa aqui, pois sei que a classe não é um usuario nem um rank
 }
}

processarClasses(new Usuario());
processarClasses(new Rank());

Além disso, podemos aplicar o conceito do narrowing por meio dos tipos null e undefined da seguinte forma:

function processarNada(valor: string | null) {
 if (valor !== null) {
 // Aqui TypeScript sabe que valor não é null
 console.log(valor.trim());
 } else {
 console.log("Valor é null");
 }
}

No caso da função acima, nós estamos verificando se o valor é uma string ou null, fazendo as tratativas de acordo com os dados recebidos 😉

Benefícios do Narrowing

O Narrowing é muito utilizado em conjunto com os Union Types, e se torna essêncial para identificar tipos de dados imprevisíveis, onde queremos executar algo diferente para cada uma das possibilidades que nos é apresentada.

Que foi o que vimos anteriormente, onde uma função que pode receber um ou mais tipos de dados dentro de uma mesma variável, usa uma condicional para melhor tratar esses dados:

function processar(valor: string | number) {
 if (typeof valor === "string") {
 //Se for uma string, eu trato esse dado como uma string...
 } else {
 //Se for um number, eu trato esse dado como um number...
 }
}

O que se torna primordial para evitar erros do compilador, por que imagina o seguinte cenário:

function processar(valor: string | number) {
 console.log('Primeiro Caractere é: ' + valor.charAt(0));
}

processar("Micilini Roll")
processar("https://micilini.com/");

De acordo com a lógica acima, apesar da função processar estar recebendo um parâmetro que pode ser uma string ou um number, ela processa o valor como se ele fosse exclusivamente uma string (o que não pode ser verdade, pois uma hora ou outra pode aparecer um number alí).

Tudo bem que eu estou chamando a função processar sempre informando strings ("Micilini Roll" e "https://micilini.com"), e enquanto isso for verdade, a nossa aplicação não irá enfrentar muitos problemas.

Mas se por um acaso eu forneça um number... a aplicação vai gerar um erro!

function processar(valor: string | number) {
 console.log('Primeiro Caractere é: ' + valor.charAt(0));
}

processar("Micilini Roll")
processar("https://micilini.com/");
processar(100);

E é por esse motivo que o mais viável a se fazer é usar o conceito do Narrowing, de modo a deixar a nossa aplicação mais segura e previsível!

function processar(valor: string | number) {
 if(typeof valor === "string"){
 console.log('Primeiro Caractere é: ' + valor.charAt(0));
 }else{
 console.log('O número informado é: ' + valor);
 }
}

processar("Micilini Roll")
processar("https://micilini.com/");
processar(100);

Portanto, o narrowing se faz necessário principalmente quando estamos trabalhando com APIs de terceiros que podem retornar diversos tipos de dados diferentes, e como na maioria das vezes não fomos nós que criamos essa API, o narrowing se torna fundamental nesse aspecto.

Narrowing com operador in

Também podemos aplicar o conceito do narrowing usando o operador in do Javascript da seguinte forma:

function descrever(serVivo: { nome: string; idade?: number; especie?: string }): string {
 if ('idade' in serVivo) {
 // Aqui, o TypeScript sabe que serVivo tem a propriedade idade
 return `A pessoa ${serVivo.nome} tem ${serVivo.idade} anos.`;
 } else if ('especie' in serVivo) {
 // Aqui, o TypeScript sabe que serVivo tem a propriedade especie
 return `O animal ${serVivo.nome} é da espécie ${serVivo.especie}.`;
 } else {
 return 'Tipo de ser vivo desconhecido.';
 }
}

// Exemplos de uso
const pessoa = { nome: 'Ana', idade: 30 };
const animal = { nome: 'Rex', especie: 'cachorro' };

console.log(descrever(pessoa)); // Saída: A pessoa Ana tem 30 anos.
console.log(descrever(animal)); // Saída: O animal Rex é da espécie cachorro.

Lembrando que o operador in é usado para checar se existe uma propriedade dentro de um objeto.

Veja um outro exemplo usando interfaces/classes, uma vez que elas também são objetos 😉

interface PessoaI {
 nome: string;
 idade: number;
}
 
interface AnimalI {
 nome: string;
 especie: string;
}
 
type SerVivo = PessoaI | AnimalI;
 
function descreverUm(serVivo: SerVivo): string {
 if ('idade' in serVivo) {
 // Aqui o TypeScript sabe que serVivo é do tipo Pessoa
 return `A pessoa ${serVivo.nome} tem ${serVivo.idade} anos.`;
 } else if ('especie' in serVivo) {
 // Aqui o TypeScript sabe que serVivo é do tipo Animal
 return `O animal ${serVivo.nome} é da espécie ${serVivo.especie}.`;
 } else {
 return 'Tipo de ser vivo desconhecido.';
 }
}
 
const pessoaUm: PessoaI = { nome: 'Ana', idade: 30 };
const animalUm: AnimalI = { nome: 'Rex', especie: 'cachorro' };
 
console.log(descreverUm(pessoaUm)); // Saída: A pessoa Ana tem 30 anos.
console.log(descreverUm(animalUm)); // Saída: O animal Rex é da espécie cachorro.

Arquivos da lição

Os arquivos desta lição podem ser encontrados no repositório do GitHub por meio deste link.

Conclusão

Nesta lição reforçamos o entendimento sobre um recurso que o Typescript tem para identificar e refinar tipos de dados que uma variável pode receber, tal recurso conhecido como Narrowing.

Até a próxima lição 🤩