search

Como dominar Conditional Types no TypeScript

Como dominar Conditional Types no TypeScript

Como dominar Conditional Types no TypeScript

Conditional Types são um dos recursos mais poderosos do TypeScript para criar tipos que se adaptam com base em condições. Em projetos reais, eles permitem modelar APIs mais seguras, inferir tipos automaticamente e reduzir duplicação de tipagem — desde que usados com critério, porque também podem tornar a base de tipos mais difícil de entender e depurar.

Neste guia, você vai aprender o que são Conditional Types, como eles funcionam (incluindo distributive conditional types), como combiná-los com infer e quais padrões práticos ajudam a manter legibilidade e previsibilidade.


O que são Conditional Types

Um Conditional Type tem a forma:

T extends U ? X : Y

A leitura é: “Se T for atribuível a U, o tipo resultante é X; caso contrário, é Y”.

Exemplo simples:

type IsString<T> = T extends string ? true : false;

type A = IsString<"ok">; // true
type B = IsString<123>; // false

Isso é útil quando você quer que o tipo final dependa do tipo de entrada, sem precisar criar múltiplas sobrecargas ou tipos manualmente.


Entendendo “extends” aqui: não é herança

No contexto de Conditional Types, extends significa “é atribuível a” (compatível), não herança.

Exemplo:

type X = "a" extends string ? 1 : 2; // 1
type Y = string extends "a" ? 1 : 2; // 2
  • "a" é atribuível a string → verdadeiro
  • string não é atribuível ao literal "a" → falso

Essa diferença é central para prever resultados.


O comportamento distributivo (e por que ele pega muita gente)

Quando T é um tipo genérico “nu” (aparece sozinho no lado esquerdo: T extends ...), o TypeScript distribui o condicional sobre unions.

type ToArray<T> = T extends any ? T[] : never;

type R = ToArray<string | number>; // string[] | number[]

Ele se comporta como se fosse:

  • ToArray<string> | ToArray<number>

Esse comportamento é extremamente útil, mas também pode causar surpresas.

Como “desligar” a distribuição

Para impedir distribuição, envolva o tipo em uma tupla:

type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;

type R2 = ToArrayNonDistributive<string | number>; // (string | number)[]

Use isso quando você quer tratar a union como um todo, e não como partes.


Conditional Types com infer: extraindo tipos automaticamente

Cuidados Onde Conditional Types Podem Virar Um Problema

O operador infer permite capturar um tipo dentro de uma condição. Isso é comum ao trabalhar com funções, Promises, arrays e estruturas genéricas.

Exemplo: extrair tipo resolvido de Promise

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<number>>; // number
type B = UnwrapPromise<string>; // string

Exemplo: extrair tipo do item de um array

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

type E1 = ElementType<string[]>; // string
type E2 = ElementType<number>; // never

O infer é um dos pilares para criar utility types robustos.


Padrões práticos para usar Conditional Types com segurança

A seguir, padrões comuns e aplicáveis no dia a dia.

1) Criar tipos de API que mudam conforme entrada

Suponha uma função que aceita um endpoint e retorna dados diferentes:

type Endpoint = "users" | "orders";

type ApiResponse<E extends Endpoint> = E extends "users"
  ? { id: string; name: string }[]
  : E extends "orders"
  ? { id: string; total: number }[]
  : never;

Uso:

type Users = ApiResponse<"users">; // { id; name }[]
type Orders = ApiResponse<"orders">; // { id; total }[]

Esse modelo evita que você “esqueça” de atualizar tipos de retorno quando adiciona novos endpoints (desde que você trate o never e mantenha a union sincronizada).

2) Filtrar unions (padrão “Filter”)

Você pode manter apenas membros de uma union que atendam a uma condição:

type OnlyStrings<T> = T extends string ? T : never;

type R = OnlyStrings<string | number | boolean>; // string

Esse padrão é a base de vários utilitários (incluindo ideias semelhantes ao Extract e Exclude).

3) Mapear unions para formatos diferentes

Você pode “converter” cada membro:

type Kind<T> = T extends string
  ? "text"
  : T extends number
  ? "numeric"
  : "other";

type K = Kind<string | number | Date>; // "text" | "numeric" | "other"

Aqui, a distribuição é desejada: cada membro vira uma saída.

4) Condicionais para propriedades opcionais

Em modelos de dados, é comum tornar propriedades opcionais dependendo de um “modo”:

type Mode = "create" | "update";

type Payload<M extends Mode> = M extends "create"
  ? { name: string; email: string }
  : { id: string; name?: string; email?: string };

Esse padrão ajuda a reforçar regras de negócio diretamente no tipo.


Conditional Types e funções: tipando retorno com base em parâmetros

Uma aplicação frequente é relacionar entrada e saída em funções genéricas.

Exemplo: função que recebe string | number e retorna tipo diferente:

type ParseResult<T> = T extends string ? number : string;

declare function parse<T extends string | number>(value: T): ParseResult<T>;

const a = parse("123"); // a: number
const b = parse(123); // b: string

Aqui, o Conditional Types permite que o retorno acompanhe o tipo real passado.


Cuidados: onde Conditional Types podem virar um problema

1) Complexidade e depuração

Tipos condicionais aninhados ficam difíceis de ler:

type Complex<T> = T extends string
  ? string extends T
    ? "wide-string"
    : "literal"
  : T extends number
  ? "number"
  : "other";

Se você precisa de 3+ níveis de aninhamento, considere dividir em tipos menores:

type StringKind<T> = T extends string
  ? string extends T
    ? "wide-string"
    : "literal"
  : never;
type Kind2<T> = StringKind<T> extends never
  ? T extends number
    ? "number"
    : "other"
  : StringKind<T>;

Não fica “curto”, mas fica mais rastreável.

2) Distribuição inesperada com unions

Se você não pretende distribuição, use o truque da tupla ([T] extends [...]).

Erros comuns surgem em validações e transformações onde o desenvolvedor esperava um tipo único, mas recebeu uma union de resultados.

3) any e unknown mudam o jogo

  • any tende a “infectar” resultados, frequentemente tornando a checagem pouco útil.
  • unknown exige refinamento.

Exemplo:

type RAny<T> = T extends string ? 1 : 2;

type A = RAny<any>; // 1 | 2 (comportamento que pode surpreender)
type U = RAny<unknown>; // 2

Em bases de código com tipagem frouxa, Conditional Types podem perder precisão. Em contrapartida, em código bem tipado, eles brilham.


Passo a passo: como pensar e escrever um Conditional Type

  1. Defina a entrada (T) e o que você quer decidir
    Ex.: “se for Promise, extrair o valor”.

  2. Escreva a condição mínima
    T extends Promise<...> ? ... : ...

  3. Use infer se você precisa capturar um tipo interno
    Promise<infer U>

  4. Teste com casos simples e com unions
    Inclua casos como string | number para verificar distribuição.

  5. Decida se a distribuição é desejada
    Se não for, aplique [T] extends [U].

  6. Refatore se passar de um limite de complexidade
    Quebre em tipos auxiliares e nomeie as etapas.


Boas práticas para projetos grandes

  • Documente tipos condicionais complexos com comentários curtos e exemplos de uso (principalmente em libs internas).
  • Prefira nomes descritivos (UnwrapPromise, ElementType, ApiResponse) a nomes genéricos (Result, X).
  • Evite aninhamento profundo: divida em tipos intermediários.
  • Garanta testes de tipo (quando possível) usando padrões como satisfies e validações de compilação, para evitar regressões de tipagem.
  • Trate o caso “impossível” com never de forma intencional: ele é um aliado para detectar combinações inválidas.

Conclusão

Conditional Types são uma ferramenta central para tipagem avançada em TypeScript: permitem modelar regras, extrair tipos, transformar unions e criar APIs com alto nível de segurança e inferência. O domínio vem de entender dois pontos críticos: distribuição em unions e o uso correto de infer.

Ao aplicar padrões práticos, limitar complexidade e controlar a distribuição quando necessário, você consegue tirar o máximo proveito dos Conditional Types sem sacrificar legibilidade — um equilíbrio essencial em projetos que precisam evoluir com rapidez e segurança.

Compartilhar este artigo: