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.
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:
function etiqueta(codigo: string | number): string {
return codigo.toUpperCase();
}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:
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 '));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:
function nomeDaConta(nome: string | null): string {
if (nome === null) {
return 'Conta sem nome';
}
return nome.toUpperCase();
}
console.log(nomeDaConta(null));
console.log(nomeDaConta('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.
function mostrarQuantidade(valor: number | null): string {
if (!valor) return 'sem quantidade';
return `${valor} unidades`;
}
console.log(mostrarQuantidade(0));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:
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' }));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:
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'));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:
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' }));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:
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' }));Acrescente à union { tipo: 'motoboy'; telefone: string } sem criar outro case.
O default deixa de receber 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:
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));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á.
Perguntas frequentes
O que é narrowing em TypeScript?
Type guard e narrowing são a mesma coisa?
Posso usar as para evitar um type guard?
Quando usar uma union discriminada?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- TypeScript Handbook — Narrowing — typescriptlang.org
- TypeScript Handbook — Everyday Types — typescriptlang.org
- TypeScript Handbook — The Never Type — typescriptlang.org


