Infer: extraindo tipos dentro de conditional types

1. Introdução ao infer e Conditional Types

Os conditional types são uma das features mais poderosas do sistema de tipos do TypeScript. Sua sintaxe básica segue o padrão T extends U ? X : Y, onde avaliamos se um tipo T é atribuível a U e, dependendo do resultado, retornamos X ou Y. É aqui que o operador infer entra em cena como um mecanismo de "decomposição" de tipos.

O infer permite que você declare uma variável de tipo dentro de uma cláusula extends e a utilize no ramo verdadeiro da condicional. Diferente de keyof (que extrai chaves de um objeto) ou typeof (que captura o tipo de um valor em tempo de execução), o infer opera em tempo de compilação dentro de conditional types, possibilitando a extração de partes internas de tipos complexos.

2. Sintaxe Fundamental do infer

A estrutura básica do infer é simples:

type ExtractType<T> = T extends infer R ? R : never;

Neste exemplo, infer R captura o tipo T e o expõe como R. Embora trivial, isso demonstra o posicionamento do infer dentro de uma cláusula extends. É importante notar que infer só pode ser usado em posições de tipo dentro de uma condicional — você não pode usá-lo fora desse contexto.

// Exemplo prático
type Example = ExtractType<string>; // string
type Example2 = ExtractType<number[]>; // number[]

3. Extraindo Tipos de Funções

Um dos usos mais comuns do infer é extrair tipos de funções. O TypeScript já fornece utilitários como ReturnType<T> e Parameters<T>, que são implementados internamente com infer:

// Implementação manual de ReturnType
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Implementação manual de Parameters
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

// Uso prático
function getUser(id: number): Promise<{ name: string; age: number }> {
  return Promise.resolve({ name: "Alice", age: 30 });
}

type UserReturn = MyReturnType<typeof getUser>; // Promise<{ name: string; age: number }>
type UserParams = MyParameters<typeof getUser>; // [id: number]

Para funções assíncronas ou com overloads, o infer se mostra ainda mais útil:

// Extraindo o tipo resolvido de uma Promise
type AsyncReturnType<T> = T extends (...args: any[]) => Promise<infer U> ? U : never;

type ResolvedUser = AsyncReturnType<typeof getUser>; // { name: string; age: number }

4. Extraindo Tipos de Arrays e Tuplas

O infer também é excelente para trabalhar com arrays e tuplas:

// Extraindo o tipo dos elementos de um array
type ArrayElement<T> = T extends (infer U)[] ? U : never;

type Numbers = ArrayElement<number[]>; // number
type Strings = ArrayElement<string[]>; // string

// Extraindo o primeiro elemento de uma tupla
type FirstElement<T> = T extends [infer First, ...any[]] ? First : never;

type First = FirstElement<[string, number, boolean]>; // string

// Extraindo o último elemento de uma tupla
type LastElement<T> = T extends [...any[], infer Last] ? Last : never;

type Last = LastElement<[string, number, boolean]>; // boolean

Para casos mais avançados, como arrays aninhados:

type DeepArrayElement<T> = T extends (infer U)[]
  ? U extends any[] ? DeepArrayElement<U> : U
  : never;

type Nested = DeepArrayElement<number[][][]>; // number

5. Inferência em Tipos de Objetos e Promises

Objetos e Promises são alvos frequentes de extração com infer:

// Extraindo tipos de propriedades de objetos
type PropertyType<T, K extends keyof T> = T extends { [P in K]: infer V } ? V : never;

interface User {
  id: number;
  name: string;
  email: string;
}

type UserName = PropertyType<User, 'name'>; // string

// Inferindo o tipo resolvido de uma Promise (similar ao Awaited nativo)
type MyAwaited<T> = T extends Promise<infer U> ? U : T;

type PromiseString = MyAwaited<Promise<string>>; // string
type DirectNumber = MyAwaited<number>; // number

// Combinação com keyof para extrair múltiplas propriedades
type ValuesOf<T> = T extends { [K in keyof T]: infer V } ? V : never;

type UserValues = ValuesOf<User>; // string | number

6. Técnicas Avançadas com infer Recursivo

O infer recursivo permite manipular tipos complexos de forma elegante:

// DeepReadonly usando infer recursivo
type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

interface NestedObject {
  user: {
    name: string;
    address: {
      street: string;
      city: string;
    };
  };
  tags: string[];
}

type ReadonlyNested = DeepReadonly<NestedObject>;
// Torna todas as propriedades e arrays readonly recursivamente

// DeepFlatten usando infer
type DeepFlatten<T> = T extends (infer U)[]
  ? DeepFlatten<U>
  : T extends object
    ? { [K in keyof T]: DeepFlatten<T[K]> }
    : T;

Cuidado com recursão infinita — o TypeScript impõe limites de profundidade para evitar travamentos no compilador.

7. Padrões e Boas Práticas com infer

Ao usar infer, considere estas práticas:

// Prefira utilitários nativos quando possível
type Return = ReturnType<typeof someFunction>; // Melhor que implementar manualmente

// Nomeie variáveis de inferência de forma descritiva
type ExtractConfig<T> = T extends { config: infer ConfigType } ? ConfigType : never;

// Evite inferência ambígua
type Ambiguous<T> = T extends (infer U)[] | infer U ? U : never; // Pode causar comportamentos inesperados

// Trate casos de never explicitamente
type SafeReturn<T> = T extends (...args: any[]) => infer R
  ? R extends never ? never : R
  : never;

8. Exemplos do Mundo Real e Conclusão

No mundo real, o infer brilha em bibliotecas e frameworks:

// Extraindo tipos de eventos do React
type EventHandler<T> = T extends (event: infer E) => void ? E : never;

type ClickEvent = EventHandler<(e: React.MouseEvent<HTMLButtonElement>) => void>;
// React.MouseEvent<HTMLButtonElement>

// Extraindo tipos de actions do Redux
type ExtractActionPayload<T> = T extends { type: string; payload: infer P } ? P : never;

interface AddUserAction {
  type: 'ADD_USER';
  payload: { id: number; name: string };
}

type AddUserPayload = ExtractActionPayload<AddUserAction>; // { id: number; name: string }

// Integração com template literal types para parsing de strings
type ExtractPathParams<T extends string> = 
  T extends `${infer Start}/:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractPathParams<Rest>]: string }
    : T extends `${infer Start}/:${infer Param}`
      ? { [K in Param]: string }
      : {};

type RouteParams = ExtractPathParams<'/users/:id/posts/:postId'>;
// { id: string; postId: string }

O operador infer é uma ferramenta essencial para metaprogramação de tipos em TypeScript. Ele permite decompor tipos complexos, extrair informações internas e construir abstrações poderosas que seriam impossíveis com outras abordagens. Dominar o infer eleva significativamente sua capacidade de criar tipos flexíveis e reutilizáveis, tornando seu código mais seguro e expressivo.

Referências