Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Narrowing no TypeScript: como estreitar tipos

Use typeof, in, instanceof, unions discriminadas e type predicates para provar qual valor chegou e liberar operações com segurança no TypeScript.

Rodolfo Mori5 min de leitura

Narrowing é o processo pelo qual o TypeScript reduz um tipo amplo, como string | number, para uma alternativa específica depois que seu código apresenta uma prova. Você faz uma verificação que também funciona em JavaScript; o compilador acompanha o fluxo e passa a liberar somente as operações válidas naquele ramo.

Ao final, você saberá escolher entre typeof, igualdade, in, instanceof, uma propriedade discriminante e um predicado personalizado. O ponto não é decorar seis sintaxes. É olhar para a origem do valor e produzir a menor prova que separa os casos reais. As unions usadas aqui foram apresentadas em tipos básicos de TypeScript.

A triagem antes do atendimento: o modelo mental

Numa recepção, a pergunta “você veio para consulta ou exame?” reduz um grupo amplo a uma fila específica. Depois da resposta, o atendente pede documentos e orientações próprios daquele serviço. Antes da triagem, agir como se todo mundo fosse paciente de consulta produziria erros.

No mapeamento técnico, a fila inicial é a union, a pergunta é o type guard, o caminho escolhido é o ramo de controle e as instruções liberadas são os métodos daquele tipo. O resultado que o compilador enxerga chama-se narrowing. O limite da analogia: no programa a “resposta” precisa ser uma condição confiável executada sobre o valor; uma assertion com as apenas manda o compilador confiar e não faz triagem nenhuma.

Esse detalhe traz a gente de volta ao comportamento: TypeScript usa análise de fluxo de controle. Retornos, atribuições e condições mudam o conjunto de tipos possíveis em cada ponto do código.

Por que a union bloqueia uma operação específica

Considere um identificador que pode chegar como texto ou número:

ts
function etiqueta(codigo: string | number): string {
  return codigo.toUpperCase();
}
error TS2339: Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.

O método existe em string, mas não em number. Uma union não significa “escolha o tipo que quiser agora”; significa que, até existir evidência, qualquer alternativa declarada pode ter chegado. O compilador protege justamente o caso em que etiqueta(42) tentaria chamar um método inexistente.

Silenciar com codigo as string mudaria apenas a visão do compilador. O número continuaria número no Node. A correção é tratar os dois caminhos.

Typeof separa os primitivos

typeof é um operador de JavaScript e também um type guard reconhecido:

ts
function etiqueta(codigo: string | number): string {
  if (typeof codigo === 'number') {
    return codigo.toString().padStart(6, '0');
  }

  return codigo.trim().toUpperCase();
}

console.log(etiqueta(42));
console.log(etiqueta(' ab-9 '));
000042 AB-9

Dentro do if, codigo é number. Como aquele caminho termina com return, o resto da função recebe apenas a alternativa string. Esse segundo estreitamento acontece por análise de alcançabilidade, não por outro else obrigatório.

Use typeof para string, number, boolean, bigint, symbol, undefined e function. Lembre que typeof null devolve 'object' por uma particularidade histórica do JavaScript; para null, faça comparação direta.

Igualdade torna ausência explícita

Quando o contrato diferencia null, compare esse valor:

ts
function nomeDaConta(nome: string | null): string {
  if (nome === null) {
    return 'Conta sem nome';
  }

  return nome.toUpperCase();
}

console.log(nomeDaConta(null));
console.log(nomeDaConta('Ana'));
Conta sem nome ANA

Um teste if (!nome) também retiraria null, mas junto trataria a string vazia como ausência. Isso pode ou não corresponder à regra. Truthiness narrowing é o nome técnico do estreitamento por verdadeiro/falso; a tradução prática é que valores como '', 0, false, null e undefined caem no mesmo lado. Prefira igualdade explícita quando esses casos precisam de respostas diferentes.

ts
function mostrarQuantidade(valor: number | null): string {
  if (!valor) return 'sem quantidade';
  return `${valor} unidades`;
}

console.log(mostrarQuantidade(0));
sem quantidade

O código compila, mas talvez a regra esteja errada: zero é uma quantidade válida e observável. Narrowing garante coerência de tipo, não interpreta sua intenção.

In pergunta qual propriedade existe

Para objetos com formatos diferentes, o operador in testa a presença de uma chave em runtime e estreita a union:

ts
type PagamentoCartao = {
  ultimosDigitos: string;
};

type PagamentoPix = {
  chavePix: string;
};

function comprovante(pagamento: PagamentoCartao | PagamentoPix): string {
  if ('chavePix' in pagamento) {
    return `Pix para ${pagamento.chavePix}`;
  }

  return `Cartão final ${pagamento.ultimosDigitos}`;
}

console.log(comprovante({ chavePix: 'loja@example.com' }));
Pix para loja@example.com

O teste é adequado porque chavePix separa as formas. Se a propriedade for opcional nos dois lados, ela deixa de ser um bom discriminante: o compilador precisa manter alternativas onde a chave pode existir. Modele diferenças reais, em vez de tornar todas as propriedades opcionais num objeto gigante.

Instanceof usa a cadeia de protótipos

instanceof verifica se um objeto foi criado a partir de um construtor na cadeia de protótipos. É útil com classes e objetos nativos como Date:

ts
function formatarData(valor: Date | string): string {
  if (valor instanceof Date) {
    return valor.toISOString().slice(0, 10);
  }

  return new Date(valor).toISOString().slice(0, 10);
}

console.log(formatarData(new Date('2026-08-22T12:00:00Z')));
console.log(formatarData('2026-08-23T12:00:00Z'));
2026-08-22 2026-08-23

Uma interface não existe em runtime, portanto não cabe em instanceof. Se os dados vieram de JSON, uma data normalmente chegou como string, não como instância de Date. Verifique a representação real da fronteira antes de escolher o guard.

Uma propriedade discriminante organiza estados

Uma discriminated union reúne objetos que compartilham uma chave literal, mas possuem dados diferentes. Para um pagamento, status pode separar os casos:

ts
type Pagamento =
  | { status: 'pendente'; criadoEm: string }
  | { status: 'aprovado'; aprovadoEm: string; codigo: string }
  | { status: 'recusado'; motivo: string };

function mensagem(pagamento: Pagamento): string {
  switch (pagamento.status) {
    case 'pendente':
      return `Criado em ${pagamento.criadoEm}`;
    case 'aprovado':
      return `Aprovado: ${pagamento.codigo}`;
    case 'recusado':
      return `Recusado: ${pagamento.motivo}`;
  }
}

console.log(mensagem({ status: 'aprovado', aprovadoEm: '2026-08-22', codigo: 'OK42' }));
Aprovado: OK42

No caso 'aprovado', codigo existe; no pendente, não. O formato impede estados impossíveis, como “pendente com motivo de recusa”, de circularem como se fossem normais. Interface e type explica por que type alias é a escolha natural para essa union.

Never transforma caso esquecido em erro

Depois que todos os casos foram tratados, o valor restante tem tipo never: não há alternativa possível. Uma função auxiliar torna essa exaustividade explícita:

ts
function casoInesperado(valor: never): never {
  throw new Error(`Caso não tratado: ${JSON.stringify(valor)}`);
}

type Entrega =
  | { tipo: 'retirada'; loja: string }
  | { tipo: 'transportadora'; rastreio: string };

function destino(entrega: Entrega): string {
  switch (entrega.tipo) {
    case 'retirada':
      return entrega.loja;
    case 'transportadora':
      return entrega.rastreio;
    default:
      return casoInesperado(entrega);
  }
}

console.log(destino({ tipo: 'retirada', loja: 'Paulista' }));
Paulista

Acrescente à union { tipo: 'motoboy'; telefone: string } sem criar outro case. O default deixa de receber never:

error TS2345: Argument of type '{ tipo: "motoboy"; telefone: string; }' is not assignable to parameter of type 'never'.

O erro aponta todos os switch exaustivos quando um novo estado entra. Isso transforma uma mudança de regra em lista concreta de lugares para revisar.

Type predicate reaproveita uma verificação

Quando a mesma regra aparece várias vezes, uma função pode devolver um type predicate na forma valor is Tipo:

ts
type Produto = {
  nome: string;
  preco: number;
};

function ehProduto(valor: unknown): valor is Produto {
  if (typeof valor !== 'object' || valor === null) return false;

  return 'nome' in valor
    && typeof valor.nome === 'string'
    && 'preco' in valor
    && typeof valor.preco === 'number';
}

const dados: unknown[] = [
  { nome: 'Mouse', preco: 180 },
  { nome: 'Teclado', preco: '250' },
];

const produtos = dados.filter(ehProduto);
console.log(produtos.map((produto) => produto.nome));
[ 'Mouse' ]

O predicado não é prova automática de que a implementação está certa; você assume a responsabilidade de testar todas as propriedades necessárias. Aqui ele protege runtime e ensina ao compilador que o array filtrado contém Produto. Para schemas grandes, uma biblioteca de validação reduz código repetido.

Missão: feche todos os caminhos de uma entrega

Modele Entrega com três casos: retirada tem loja, correio tem rastreio e motoboy tem telefone. Escreva instrucoes(entrega) usando a propriedade tipo, acrescente uma checagem exaustiva com never e imprima uma instrução de cada caso.

Seu critério de sucesso tem três partes: cada saída usa apenas propriedades do caso correto; retirar um case gera erro no ponto exaustivo; acrescentar entrega.rastreio no ramo motoboy gera propriedade inexistente. Depois receba um valor unknown e crie uma verificação mínima antes de passá-lo à função.

Não use as Entrega para vencer a missão. A prática existe para transformar evidência de runtime em certeza local. Quando os guards fizerem sentido, avance para generics em TypeScript, onde a gente preserva relações entre tipos sem saber antecipadamente qual tipo concreto virá.

  • typescript
  • narrowing
  • type guard
  • union types
  • type predicate

Perguntas frequentes

O que é narrowing em TypeScript?
É o estreitamento de um tipo amplo para uma alternativa mais específica depois de uma verificação. O compilador acompanha o fluxo e libera apenas as operações seguras em cada caminho.
Type guard e narrowing são a mesma coisa?
Não exatamente. Type guard é uma verificação reconhecida, como typeof ou um predicado. Narrowing é o resultado dessa evidência no tipo que o compilador enxerga dentro de cada ramo.
Posso usar as para evitar um type guard?
Você pode fazer uma assertion, mas ela não verifica o valor em runtime. Quando existem alternativas reais, prefira um teste observável que também proteja a execução.
Quando usar uma union discriminada?
Quando cada estado conhecido possui dados próprios. Uma propriedade literal compartilhada, como tipo ou status, permite selecionar o caso e checar se todos os estados foram tratados.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0 com TypeScript 7.0.2, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. TypeScript Handbook — Narrowing — typescriptlang.org
  2. TypeScript Handbook — Everyday Types — typescriptlang.org
  3. TypeScript Handbook — The Never Type — typescriptlang.org

Continue por aqui