Back

A Revolução do TypeScript 5.5: Type Predicates Inferidos Explicados

A Revolução do TypeScript 5.5: Type Predicates Inferidos Explicados

Se você já escreveu TypeScript por algum tempo, provavelmente já escreveu código assim:

function isString(value: unknown): value is string { return typeof value === 'string'; }

Aquela parte value is string? Chama-se type predicate. E por anos, você tinha que escrever isso manualmente toda vez que queria que o TypeScript entendesse que sua função estreita tipos.

TypeScript 5.5 muda tudo.

Com o novo recurso Inferred Type Predicates, o TypeScript agora pode inferir automaticamente esses type predicates do corpo da sua função. Sem mais anotações manuais. Sem esquecer de adicioná-las. O compilador simplesmente... descobre.

Isso não é uma pequena melhoria de qualidade de vida. É uma mudança fundamental em como escrevemos código type-safe. Vamos mergulhar fundo no que mudou, como funciona, e por que seu codebase está prestes a ficar muito mais limpo.

O Problema: Type Predicates Manuais Eram Tediosos e Propensos a Erros

Antes do TypeScript 5.5, o estreitamento de tipos só funcionava implicitamente dentro do corpo de uma função. No momento em que você extraía essa lógica para uma função separada, a informação de tipo se perdia:

// Isso funciona - estreitamento de tipo inline const values: (string | number)[] = ['a', 1, 'b', 2]; const strings = values.filter(value => typeof value === 'string'); // TypeScript 5.4: strings é (string | number)[] ❌ // TypeScript 5.5: strings é string[] ✅

No TypeScript 5.4 e anteriores, mesmo que o callback do filter claramente só retorne true para strings, o TypeScript não conseguia carregar essa informação de tipo. O resultado era tipado como (string | number)[], que é tecnicamente correto mas praticamente inútil.

Para corrigir isso, você tinha que escrever um type predicate explícito:

function isString(value: string | number): value is string { return typeof value === 'string'; } const strings = values.filter(isString); // strings é string[] ✅

Isso parece ok para um exemplo simples, mas em codebases reais:

  1. Você esquece de adicionar type predicates e fica se perguntando por que os tipos não estão estreitando
  2. Type predicates podem mentir - nada impede você de escrever value is string enquanto retorna typeof value === 'number'
  3. O boilerplate explode quando você tem dezenas de funções type guard

A Solução: TypeScript 5.5 Infere Type Predicates Automaticamente

A partir do TypeScript 5.5, o compilador analisa o corpo da sua função e automaticamente infere um type predicate quando:

  1. A função não tem tipo de retorno explícito ou anotação de type predicate
  2. A função tem uma única instrução return (ou múltiplos returns com a mesma lógica de estreitamento)
  3. A função não muta seu parâmetro
  4. A função retorna uma expressão booleana que estreita o tipo do parâmetro

Vamos ver isso em ação:

// TypeScript 5.5 - Não precisa de anotação! function isString(value: unknown) { return typeof value === 'string'; } // TypeScript infere automaticamente: (value: unknown) => value is string const values: unknown[] = ['hello', 42, 'world', null]; const strings = values.filter(isString); // strings é string[] ✅

O compilador olha a instrução return, vê typeof value === 'string', e automaticamente gera o type predicate value is string.

Mergulho Profundo: Quando a Inferência é Acionada?

A inferência do TypeScript 5.5 não é mágica—segue regras específicas. Entender essas regras ajuda você a escrever código que funciona perfeitamente com o novo recurso.

Regra 1: A Função Deve Retornar um Booleano

Type predicates só fazem sentido para funções que retornam booleanos. O TypeScript só inferirá predicates para funções que retornem boolean:

// ✅ Inferência funciona function isNumber(x: unknown) { return typeof x === 'number'; } // ❌ Inferência não se aplica - retorna o valor, não boolean function getNumber(x: unknown) { return typeof x === 'number' ? x : null; }

Regra 2: Sem Anotação Explícita de Tipo de Retorno

Se você anotar explicitamente o tipo de retorno, o TypeScript respeita sua anotação e não inferirá um predicate:

// ❌ Sem inferência - tipo de retorno explícito bloqueia function isString(value: unknown): boolean { return typeof value === 'string'; } // ✅ Inferência funciona - sem anotação de tipo de retorno function isString(value: unknown) { return typeof value === 'string'; }

Isso é intencional—permite que você desative a inferência quando necessário.

Regra 3: O Parâmetro Não Deve Ser Mutado

O TypeScript precisa confiar que o parâmetro é o mesmo objeto antes e depois da verificação. Se você mutar, a inferência é desativada:

// ❌ Sem inferência - parâmetro é mutado function isNonEmptyArray(arr: unknown[]) { arr.push('something'); // Mutação! return arr.length > 0; } // ✅ Inferência funciona - sem mutação function isNonEmptyArray(arr: unknown[]) { return arr.length > 0; }

Regra 4: True Deve Implicar o Tipo Estreitado

O type predicate só é inferido quando retornar true genuinamente implica o tipo estreitado:

// ✅ Inferência funciona: true significa que é string function isString(x: unknown) { return typeof x === 'string'; } // ⚠️ Inferência funciona, mas cuidado com a lógica function isNotNull(x: string | null) { return x !== null; } // Inferido: x is string (quando true é retornado)

Regra 5: Métodos de Array Recebem Tratamento Especial

O método .filter() é a estrela desse recurso. O TypeScript agora estreita corretamente os tipos de arrays filtrados:

const mixed: (string | number | null)[] = ['a', 1, null, 'b', 2]; // Antes do 5.5: (string | number | null)[] // Depois do 5.5: (string | number)[] const nonNull = mixed.filter(x => x !== null); // Antes do 5.5: (string | number | null)[] // Depois do 5.5: string[] const strings = mixed.filter(x => typeof x === 'string'); // Encadeamento também funciona! const upperStrings = mixed .filter(x => typeof x === 'string') .map(s => s.toUpperCase()); // s está corretamente tipado como string

Exemplos do Mundo Real: Antes e Depois

Exemplo 1: Filtrando Propriedades Opcionais

interface User { id: string; email?: string; phone?: string; } const users: User[] = [ { id: '1', email: '[email protected]' }, { id: '2', phone: '555-1234' }, { id: '3', email: '[email protected]', phone: '555-5678' }, ]; // Antes do 5.5: Você precisava dessa função helper function hasEmail(user: User): user is User & { email: string } { return user.email !== undefined; } // Depois do 5.5: Só escreve o filtro inline const usersWithEmail = users.filter(u => u.email !== undefined); // Tipo é corretamente: (User & { email: string })[] usersWithEmail.forEach(u => { console.log(u.email.toUpperCase()); // Sem erro! email é string });

Exemplo 2: Uniões Discriminadas

type Result<T> = | { success: true; data: T } | { success: false; error: string }; const results: Result<number>[] = [ { success: true, data: 42 }, { success: false, error: 'Failed' }, { success: true, data: 100 }, ]; // Antes do 5.5 function isSuccess<T>(result: Result<T>): result is { success: true; data: T } { return result.success; } const successResults = results.filter(isSuccess); // Depois do 5.5 - Só escreve naturalmente const successResults = results.filter(r => r.success); // Tipo é { success: true; data: number }[] const sum = successResults.reduce((acc, r) => acc + r.data, 0); // Funciona!

Exemplo 3: Validação de Resposta de API

interface ApiResponse { status: number; data?: { items: string[]; }; } const responses: ApiResponse[] = await fetchMultipleEndpoints(); // Antes do 5.5: Predicate manual necessário function hasData(r: ApiResponse): r is ApiResponse & { data: { items: string[] } } { return r.status === 200 && r.data !== undefined; } // Depois do 5.5: Filtragem natural const validResponses = responses.filter( r => r.status === 200 && r.data !== undefined ); // Tipo inclui corretamente data não-opcional const allItems = validResponses.flatMap(r => r.data.items); // ✅ Sem erro

Pegadinhas e Casos Extremos

Pegadinha 1: Verificações de Truthiness Nem Sempre Estreitam

const values: (string | null | undefined)[] = ['a', null, 'b', undefined]; // ❌ Isso não estreita como esperado const truthy = values.filter(x => x); // Tipo: (string | null | undefined)[] // ✅ Seja explícito sobre o que está verificando const defined = values.filter(x => x !== null && x !== undefined); // Tipo: string[]

O problema é que x ser truthy não estreita definitivamente o tipo—strings vazias são falsy mas ainda são strings.

Pegadinha 2: O Construtor Boolean Não Funciona

const values: (string | null)[] = ['a', null, 'b']; // ❌ Não estreita - Boolean é tratado como função retornando boolean const filtered = values.filter(Boolean); // Tipo: (string | null)[] // ✅ Use arrow function em vez disso const filtered = values.filter(x => x !== null); // Tipo: string[]

Isso porque Boolean é tipado como (value?: unknown) => boolean, não como type predicate.

Pegadinha 3: Condições Complexas Podem Não Inferir

function isSpecialString(x: unknown) { if (typeof x !== 'string') return false; if (x.length < 5) return false; if (!x.startsWith('prefix')) return false; return true; } // TypeScript pode ou não inferir um type predicate aqui // dependendo da complexidade do fluxo de controle

Para lógica de validação complexa, type predicates explícitos ainda são seus amigos.

Considerações de Performance

Você pode se perguntar: essa inferência adiciona overhead no tempo de compilação?

A resposta é: mínimo. O motor de inferência de tipos do TypeScript já analisa os corpos das funções para estreitamento de tipos dentro de funções. Estender isso para inferir type predicates de retorno reutiliza a maior parte dessa maquinaria.

Em testes do mundo real, o impacto no tempo de compilação é insignificante—geralmente dentro do ruído de medição.

Guia de Migração: Atualizando para TypeScript 5.5

Passo 1: Atualizar TypeScript

npm install [email protected] --save-dev # ou yarn add [email protected] --dev # ou pnpm add [email protected] --save-dev

Passo 2: Remover Type Predicates Redundantes

Uma vez atualizado, você pode começar a remover type predicates explícitos que o TypeScript agora infere:

// Antes: Predicate explícito function isNumber(x: unknown): x is number { return typeof x === 'number'; } // Depois: Deixe o TypeScript inferir function isNumber(x: unknown) { return typeof x === 'number'; }

Cuidado: Não remova predicates cegamente. Mantenha-os quando:

  • A função tem lógica complexa que o TypeScript pode não inferir corretamente
  • Você quer documentar explicitamente o comportamento de estreitamento
  • A função é parte de uma API pública onde tipos explícitos melhoram a documentação

Passo 3: Revise Suas Chamadas a .filter()

É aqui que você verá os maiores ganhos. Procure no seu codebase:

  • Usos de Array.prototype.filter() com predicates inline
  • Funções helper usadas apenas para filtragem

Muitas dessas agora podem ser simplificadas.

A Visão Geral: A Filosofia do TypeScript

Esse recurso se alinha com a filosofia central do TypeScript: inferir o que pode ser inferido, exigir tipos explícitos apenas quando necessário.

O TypeScript tem progressivamente melhorado em inferência:

  • TS 2.1: Análise de fluxo de controle para estreitamento de tipos
  • TS 4.4: Análise de fluxo de controle para condições com alias
  • TS 4.9: O operador satisfies para melhor inferência
  • TS 5.5: Type predicates inferidos

Cada versão reduz o boilerplate que você precisa escrever enquanto mantém—e frequentemente melhora—a segurança de tipos.

Conclusão: Escreva Menos, Tipifique Mais Seguro

Os Inferred Type Predicates do TypeScript 5.5 representam uma melhoria significativa na qualidade de vida para desenvolvedores TypeScript. Ao gerar automaticamente type predicates dos corpos das funções, o compilador:

  1. Reduz boilerplate - Sem mais anotações manuais value is Type para checks simples
  2. Melhora correção - Predicates inferidos não podem mentir como os manuais podem
  3. Torna .filter() um prazer - Filtragem de arrays finalmente funciona como você sempre esperou

O caminho de atualização é suave, o impacto de performance é insignificante, e os benefícios são imediatos. Se você ainda não está no TypeScript 5.5, esse recurso sozinho é razão suficiente para atualizar.

Seu codebase está prestes a ficar mais limpo. Sua segurança de tipos está prestes a ficar mais forte. E você vai parar de escrever value is string pela centésima vez.

Bem-vindo ao futuro do TypeScript.

typescriptjavascripttype-safetyprogrammingweb-developmenttype-guardstypescript-5