Type narrowing é o TypeScript descobrindo um tipo mais específico para um valor em um ponto do código, com base nas verificações que o código já fez. Um parâmetro string | number vira string dentro de if (typeof x === "string") e number no else.
Chamar value.toFixed(2) antes da verificação seria um erro de compilação, porque toFixed não existe em string. A verificação é JavaScript comum e roda em tempo de execução; o narrowing é o compilador lendo essa verificação e ajustando o tipo. Nada a mais é gerado.
Control flow analysis
O TypeScript segue todos os caminhos de uma função: if/else, return e throw antecipados, switch, loops e os operadores de curto-circuito &&, ||, ?? e ?:. Em cada ponto, o tipo de uma variável é o que ainda é possível ali.
O estilo de retorno antecipado ("guard clauses") é o jeito mais legível de fazer narrowing: trate primeiro os casos estranhos, e o resto da função trabalha com o tipo limpo.
Todas as formas de narrowing
| Forma | Exemplo | Estreita |
|---|---|---|
| typeof | typeof x === "string" | primitivos e funções |
| Truthiness | if (x) | remove null, undefined e literais falsy |
| Igualdade | x === "a", x == null, x !== undefined | literais, null, undefined |
in | "swim" in pet | unions de objetos, por propriedade |
| instanceof | err instanceof TypeError | instâncias de classes |
Array.isArray | Array.isArray(x) | arrays contra todo o resto |
| Atribuição | x = 5 | para o tipo atribuído |
| Type predicate | function isUser(x: unknown): x is User | qualquer coisa que você consiga verificar |
| Assertion function | function assertUser(x: unknown): asserts x is User | tudo depois da chamada |
| Propriedade discriminante | switch (shape.kind) | tagged unions |
As três últimas são tratadas nas páginas sobre type guards e discriminated unions. As outras estão abaixo.
Narrowing por truthiness
if (x) remove null e undefined (e os literal types false, 0 e ""). É curto, e tem uma armadilha clássica: 0 e "" são falsy, então valores válidos acabam tratados como ausentes.
Para números e strings, compare explicitamente com undefined ou null (ou use ??). Truthiness funciona bem para objetos, arrays e funções, que nunca são falsy.
Narrowing por igualdade
===, !==, == e != estreitam os dois lados. Comparar com um literal estreita para esse literal; == null (igualdade frouxa) corresponde a null e a undefined em uma só verificação, e é o único lugar em que a igualdade frouxa é idiomática.
Comparar duas variáveis estreita as duas para o que elas podem ter em comum: se a: string | number e b: string | boolean passam em a === b, as duas são string dentro do if.
O operador in
"key" in obj estreita uma union de tipos de objeto para os membros que têm (ou podem ter) essa propriedade.
O in também funciona com unknown quando você já sabe que é um objeto: depois de typeof v === "object" && v !== null && "id" in v, o TypeScript sabe que v tem uma propriedade id do tipo unknown. Em unions que você mesmo projeta, uma propriedade-tag compartilhada (kind: "fish") é mais clara do que sondar métodos: esse padrão se chama discriminated union.
Narrowing por atribuição
Uma variável tem um tipo declarado e um tipo estreitado que acompanha as atribuições. Atribuir um valor estreita a variável para o tipo desse valor, dentro dos limites do tipo declarado.
Onde o narrowing se perde
O narrowing é local e conservador. Algumas situações o desfazem:
- Uma expressão diferente. Verificar
obj.nameestreitaobj.name(eobj["name"]), mas nãoobj[key]quandokeyé uma variávelstringem vez de um literal, nem uma cópia feita antes da verificação. - Callbacks e reatribuição. Dentro de um callback, um
letestreitado só mantém o narrowing se não for atribuído de novo depois que o callback é criado. Umconstou um parâmetro que nunca é reatribuído continua estreitado. - Verificações escondidas em helpers. Uma função
isString(x: unknown): booleannão diz nada ao compilador. Dê a ela um tipo de retorno de type predicate,x is string, e as chamadas passam a estreitar como otypeoffaz.
O compilador mostra index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. O callback pode rodar depois, após x = undefined. Apague essa última atribuição (ou copie o valor para um const dentro do if) e o código compila e imprime 5 duas vezes.
Um helper que retorna boolean pode ser corrigido declarando o que ele prova. É isso que um type guard é:
function isString(value: unknown): value is string {
return typeof value === "string";
}
Desde o TypeScript 5.5, o compilador infere esses predicates para arrow functions simples, e é por isso que list.filter((x) => x !== undefined) agora retorna um array sem undefined.
Perguntas frequentes
O que é type narrowing no TypeScript?
Narrowing é o TypeScript refinando o tipo de uma variável dentro de um bloco com base em uma verificação que o código faz. Depois de if (typeof x === "string"), um string | number é só string dentro do if e só number no else. O compilador segue if, else, return, switch, &&, || e ?: para descobrir o tipo em cada ponto, o que se chama control flow analysis.
Por que o TypeScript não está fazendo o narrowing do meu tipo?
Causas comuns: a verificação é feita em uma expressão diferente da que você usa (obj.a verificado, obj[key] usado com um key do tipo string); o valor é um let reatribuído depois que um callback foi criado, então o callback perde o narrowing; ou a verificação está escondida em um helper que retorna boolean simples em vez de um type predicate x is T.
O type narrowing funciona em tempo de execução?
As verificações sim: typeof, instanceof, in e === são JavaScript comum, que roda. O narrowing em si existe só em tempo de compilação. O TypeScript lê as suas verificações de tempo de execução e ajusta os tipos estáticos a elas, e nada é acrescentado ao JavaScript gerado.
Como fazer o narrowing de um tipo unknown no TypeScript?
Com as mesmas verificações: typeof value === "string", Array.isArray(value), value instanceof Date ou, para objetos, typeof value === "object" && value !== null && "id" in value. Para verificações reutilizáveis, escreva uma função type guard com tipo de retorno value is T.
Como filtrar undefined de um array no TypeScript?
items.filter((x) => x !== undefined) retorna T[] sem undefined desde o TypeScript 5.5, que infere o callback como type predicate. Em versões mais antigas, escreva o predicate você mesmo: items.filter((x): x is T => x !== undefined).