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 astring→ verdadeirostringnã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
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
anytende a “infectar” resultados, frequentemente tornando a checagem pouco útil.unknownexige 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
-
Defina a entrada (
T) e o que você quer decidir
Ex.: “se for Promise, extrair o valor”. -
Escreva a condição mínima
T extends Promise<...> ? ... : ... -
Use
inferse você precisa capturar um tipo interno
Promise<infer U> -
Teste com casos simples e com unions
Inclua casos comostring | numberpara verificar distribuição. -
Decida se a distribuição é desejada
Se não for, aplique[T] extends [U]. -
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
satisfiese validações de compilação, para evitar regressões de tipagem. - Trate o caso “impossível” com
neverde 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.