programandoai

typeof e instanceof: reduzindo um union type ao caso certo

Um valor string | number chega numa função. Antes de usar .toUpperCase() ou fazer conta, o TypeScript precisa saber, naquela linha exata, qual dos dois ele é.

Estudar esta lição interativa →

Lembra do union type do Módulo 3? string | number descreve bem um id de pedido que tanto pode vir como texto (de uma URL) quanto como número (direto do banco). Mas union type tem uma consequência que ainda não apareceu: dentro de uma função que recebe esse tipo, você não pode simplesmente chamar um método que só existe num dos dois lados, mesmo que, na sua cabeça, "só vai chegar string aqui". Rode o exemplo e leia o diagnóstico:

// Um union type não libera, de graça, um método que só existe num dos lados.
function formatarIdBruto(id: string | number) {
  return id.toUpperCase(); // erro: number não tem toUpperCase
}

console.log(formatarIdBruto("abc123"));

O TypeScript recusa .toUpperCase() porque number não tem esse método.

O diagnóstico está certo: id pode ser number, e number não tem .toUpperCase(). O TypeScript não vai adivinhar, pela sua intenção, que "só vai chegar string". Ele só aceita usar um método depois que o código prova, com um teste em tempo real, qual dos tipos da união está ali naquele ponto exato. Esse processo de provar e reduzir o tipo dentro de um bloco chama-se narrowing (em português, algo como "estreitamento"), e é provavelmente a ferramenta de TypeScript mais usada no dia a dia de qualquer time.

// narrowing com typeof: dentro de cada bloco, o TypeScript já sabe o tipo exato.
function formatarIdSeguro(id: string | number) {
  if (typeof id === "string") {
    return id.toUpperCase(); // aqui, id só pode ser string
  }
  return id.toFixed(0); // aqui, id só pode ser number
}

console.log(formatarIdSeguro("abc123"));
console.log(formatarIdSeguro(42));

Dentro de cada bloco do if, o TypeScript já sabe qual tipo é aquele.

Pense num detetive investigando um union type como quem investiga uma lista de suspeitos. No começo, qualquer um dos suspeitos pode ser o culpado (string | number, os dois em aberto). Cada pergunta feita, um typeof, elimina possibilidades: "seu álibi prova que você não estava lá" descarta um suspeito, e sobra só um. Dentro do bloco onde a pergunta já foi respondida, o TypeScript trata como certo que só resta aquele suspeito, aquele tipo.

O tipo muda de verdade dentro do bloco

Não é só uma checagem pontual: dentro do if (typeof id === "string"), o TypeScript passa a tratar id como string em todas as linhas daquele bloco, liberando .toUpperCase(), .trim(), qualquer método de string. Saindo do bloco, a união completa volta a valer (a menos que o bloco termine em return ou throw: aí o TypeScript entende que o resto da função só roda quando a condição foi falsa, e continua estreitando). É esse comportamento, o tipo mudando conforme o fluxo do código, que dá nome ao recurso: narrowing.

typeof funciona bem para os tipos primitivos (string, number, boolean), mas quando um dos lados da união é uma classe, como Error, o teste certo é instanceof, que confere se um valor foi criado a partir daquela classe:

// instanceof narrowing: diferenciando uma instância de classe (Error) de outro tipo.
function tratarResultadoPedido(resultado: Error | string) {
  if (resultado instanceof Error) {
    return "Falhou: " + resultado.message;
  }
  return "Sucesso: " + resultado;
}

console.log(tratarResultadoPedido(new Error("conexão perdida")));
console.log(tratarResultadoPedido("pedido criado"));
typeof null vale "object"

Uma pegadinha clássica de JavaScript que sobrevive no TypeScript: typeof null devolve "object", não "null". Se você tem um union type que inclui null e outro tipo objeto, um teste solto como typeof valor === "object" não separa os dois; é preciso um teste extra, como valor !== null, para excluir o null de verdade. Guarde essa armadilha, ela pega gente com anos de experiência.

// Sua vez! "dobrarValor" recebe string | number e tenta multiplicar por 2
// sem narrowing nenhum, o que o TypeScript recusa. Adicione um
// if (typeof valor === "number") ... else ... convertendo a string com
// Number(valor) antes de multiplicar.
function dobrarValor(valor: string | number) {
  return valor * 2;
}

console.log(dobrarValor(10));
console.log(dobrarValor("10"));

Adicione o narrowing com typeof até o diagnóstico sumir.

← Voltar ao índice do curso