Menu

Type narrowing em TypeScript: como funciona o control flow

Narrowing é como o TypeScript transforma um tipo amplo como string | number em um tipo específico dentro de um if, de um switch ou depois de um return antecipado. Todas as formas de narrowing em um só lugar: typeof, truthiness, igualdade, in, instanceof, atribuições, type predicates e discriminated unions.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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

FormaExemploEstreita
typeoftypeof x === "string"primitivos e funções
Truthinessif (x)remove null, undefined e literais falsy
Igualdadex === "a", x == null, x !== undefinedliterais, null, undefined
in"swim" in petunions de objetos, por propriedade
instanceoferr instanceof TypeErrorinstâncias de classes
Array.isArrayArray.isArray(x)arrays contra todo o resto
Atribuiçãox = 5para o tipo atribuído
Type predicatefunction isUser(x: unknown): x is Userqualquer coisa que você consiga verificar
Assertion functionfunction assertUser(x: unknown): asserts x is Usertudo depois da chamada
Propriedade discriminanteswitch (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.name estreita obj.name (e obj["name"]), mas não obj[key] quando key é uma variável string em vez de um literal, nem uma cópia feita antes da verificação.
  • Callbacks e reatribuição. Dentro de um callback, um let estreitado só mantém o narrowing se não for atribuído de novo depois que o callback é criado. Um const ou um parâmetro que nunca é reatribuído continua estreitado.
  • Verificações escondidas em helpers. Uma função isString(x: unknown): boolean nã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 o typeof faz.

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).

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR