Compreendendo o Narrowing de Tipos no TypeScript
Você escreveu um type guard, mas o TypeScript ainda reclama que a propriedade não existe. Ou você filtrou um array, mas o tipo resultante permanece como uma união. Essas frustrações surgem de uma lacuna entre como você pensa que o narrowing funciona e como a análise de fluxo de controle do TypeScript realmente opera.
Este artigo constrói um modelo mental claro do narrowing de tipos do TypeScript—como o compilador rastreia tipos através do seu código e quando ele perde essa informação.
Pontos-Chave
- O narrowing de tipos é a análise de fluxo de controle do TypeScript rastreando tipos através de caminhos de execução com base em verificações de runtime que ele pode validar
- Os mecanismos principais de narrowing incluem
typeof,instanceof, verificações de truthiness, verificações de igualdade, o operadorine uniões discriminadas - Type guards definidos pelo usuário com predicados de tipo permitem narrowing personalizado, mas o TypeScript não verifica a lógica do seu predicado
- O tipo
neverhabilita verificação de exaustividade em tempo de compilação para capturar casos não tratados - O narrowing falha através de limites de callbacks, reatribuições de propriedades e padrões complexos de aliasing
Como Funciona a Análise de Fluxo de Controle
O narrowing do TypeScript não é mágica. É o compilador seguindo os caminhos de execução do seu código e atualizando informações de tipo com base em verificações de runtime que ele pode validar.
Quando você escreve uma condicional, o TypeScript analisa o que deve ser verdadeiro em cada ramo:
function process(value: string | number) {
if (typeof value === 'string') {
// TypeScript sabe: value é string aqui
return value.toUpperCase()
}
// TypeScript sabe: value é number aqui
return value.toFixed(2)
}
O compilador vê a verificação typeof, reconhece-a como uma construção de narrowing e refina o tipo adequadamente. Isso acontece automaticamente para vários operadores JavaScript.
Mecanismos Principais de Narrowing
typeof e instanceof
O operador typeof reduz tipos primitivos de forma confiável. O TypeScript entende suas peculiaridades—incluindo que typeof null retorna "object".
function handle(x: unknown) {
if (typeof x === 'string') return x.length
if (typeof x === 'number') return x.toFixed(2)
}
Para instâncias de classes, instanceof fornece narrowing similar:
if (error instanceof TypeError) {
console.log(error.message)
}
Truthiness e Igualdade
Verificações de truthiness eliminam null e undefined:
function greet(name: string | null) {
if (name) {
return `Hello, ${name}` // name é string
}
}
Verificações de igualdade reduzem para tipos literais:
type Status = 'pending' | 'complete' | 'failed'
function handle(status: Status) {
if (status === 'complete') {
// status é 'complete'
}
}
O Operador in e Uniões Discriminadas
O operador in verifica a existência de propriedades, habilitando padrões de duck-typing:
if ('radius' in shape) {
return Math.PI * shape.radius ** 2
}
Uniões discriminadas combinam isso com tipos literais para padrões poderosos de narrowing:
type Result<T> =
| { success: true; data: T }
| { success: false; error: string }
function handle<T>(result: Result<T>) {
if (result.success) {
return result.data // TypeScript sabe que data existe
}
return result.error // TypeScript sabe que error existe
}
Discover how at OpenReplay.com.
Type Guards Definidos pelo Usuário
Quando as verificações integradas não são suficientes, predicados de tipo permitem que você defina guards personalizados:
function isString(value: unknown): value is string {
return typeof value === 'string'
}
Uma ressalva: o TypeScript não verifica a lógica do seu predicado. Você está afirmando o relacionamento—se sua verificação estiver errada, os tipos estarão errados.
O TypeScript moderno (5.5+) melhorou aqui. Array.filter agora pode inferir predicados de tipo automaticamente para callbacks simples, eliminando um ponto de dor comum:
const mixed: (string | number)[] = ['a', 1, 'b', 2]
const strings = mixed.filter(x => typeof x === 'string')
// TypeScript infere string[] em muitos casos
Verificação de Exaustividade com never
O tipo never habilita verificação de exaustividade em tempo de compilação:
type Circle = { kind: 'circle'; radius: number }
type Square = { kind: 'square'; side: number }
type Triangle = { kind: 'triangle'; base: number; height: number }
type Shape = Circle | Square | Triangle
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2
case 'square': return shape.side ** 2
case 'triangle': return 0.5 * shape.base * shape.height
default:
const _exhaustive: never = shape
return _exhaustive
}
}
Se você adicionar uma nova variante de forma, o TypeScript gera erro na atribuição never—forçando você a tratá-la.
O Que Narrowing Não É
Duas construções frequentemente confundidas com narrowing merecem esclarecimento:
Asserções de tipo (as) ignoram completamente o sistema de tipos. Elas não fazem narrowing—elas sobrescrevem.
O operador satisfies valida que uma expressão corresponde a um tipo sem alterá-lo. Útil para capturar erros, mas não é um mecanismo de narrowing.
Quando o Narrowing Falha
A análise de fluxo de controle do TypeScript tem limites. O narrowing não persiste através de:
- Limites de callbacks (embora closures com variáveis
constagora preservem narrowing no TypeScript 5.4+) - Reatribuições de propriedades entre verificações
- Padrões complexos de aliasing
Quando o narrowing falha inesperadamente, verifique se o compilador pode realmente rastrear o fluxo de controle da sua verificação até o seu uso.
Conclusão
Pense no narrowing como o TypeScript observando suas verificações de runtime e atualizando seu conhecimento adequadamente. O compilador é conservador—ele só faz narrowing quando pode provar que o refinamento é sólido.
Escreva verificações que o TypeScript possa seguir. Prefira uniões discriminadas em vez de asserções de tipo. Use verificação de exaustividade para capturar casos ausentes em tempo de compilação em vez de runtime.
O objetivo não é lutar contra o sistema de tipos, mas estruturar o código para que o narrowing funcione naturalmente.
Perguntas Frequentes
A análise de fluxo de controle do TypeScript não persiste o narrowing através de limites de callbacks porque o callback pode executar mais tarde quando o tipo da variável pode ter mudado. Para contornar isso, atribua o valor com narrowing a uma variável const antes do callback, ou use um type guard dentro do próprio callback.
Um type guard é uma verificação de runtime que o TypeScript reconhece e usa para fazer narrowing de tipos com segurança. Uma asserção de tipo usando 'as' diz ao TypeScript para tratar um valor como um tipo específico sem qualquer verificação de runtime. Type guards são mais seguros porque envolvem verificações reais, enquanto asserções podem esconder bugs se sua suposição estiver errada.
Use uniões discriminadas quando você tem tipos relacionados que compartilham uma propriedade literal comum como 'kind' ou 'type'. Elas fornecem narrowing automático em instruções switch e habilitam verificação de exaustividade. Type guards são melhores para validar dados externos ou quando você não pode modificar os tipos com os quais está trabalhando.
Isso geralmente significa que o TypeScript perdeu o rastro do seu narrowing. Causas comuns incluem verificar uma propriedade e então acessá-la através de uma referência diferente, reatribuir a variável entre verificação e uso, ou a verificação acontecer em um escopo diferente. Mova sua verificação de tipo para mais perto de onde você usa o valor, ou armazene o valor com narrowing em uma nova variável const.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.