Pular para o conteúdo
TypeScript

Conditional Types no TypeScript: Guia

Domine tipos condicionais no TypeScript. Syntax T extends U ? X : Y, distribuição, filtros com never e infer, tudo com exemplos práticos.

Por que isso é importante

Conditional Types no TypeScript: Guia. Domine tipos condicionais no TypeScript. Syntax T extends U ? X : Y, distribuição, filtros com never e infer, tudo com exemplos práticos.

O Que São Conditional Types

Conditional Types são tipos que mudam dependendo de uma condição. A syntax é parecida com o ternário do JavaScript: T extends U ? X : Y. Se T é compatível com U, o resultado é X. Senão, é Y.

O extends aqui NÃO significa herança. Significa 'é atribuível a'. Quando você escreve string extends object, tá perguntando: 'dá pra atribuir string onde espera object?'. A resposta define qual branch do tipo é usado.

A parte interessante é a distribuição. Quando o T é um union type, o TypeScript aplica a condição pra cada membro separadamente. Isso é o que torna conditional types tão poderosos pra filtrar e transformar unions.

Com a keyword infer, você ainda consegue extrair partes de um tipo dentro da condição. Por exemplo: pegar o tipo de retorno de uma função, ou o tipo dos elementos de um array. Tudo em tempo de compilação.

Como Usar Conditional Types Passo a Passo

Vamos do básico ao avançado. Cada passo constrói em cima do anterior.

  1. Passo 1 - Syntax básica: Escreva type IsString<T> = T extends string ? 'sim' : 'nao'. Isso cria um tipo que retorna 'sim' se T for string, 'nao' se não for.
  2. Passo 2 - Entenda extends: extends em conditional types verifica compatibilidade. number extends object é false. string extends string | number é true.
  3. Passo 3 - Distribuição com unions: Quando você passa string | number pra IsString<T>, o resultado é 'sim' | 'nao' porque cada membro é avaliado separadamente.
  4. Passo 4 - Filtrar com never: Se um branch retorna never, ele é removido do union. Isso é como filtrar: T extends string ? T : never mantém só strings.
  5. Passo 5 - Usar infer: T extends Array<infer U> ? U : never extrai o tipo dos elementos do array. infer captura um tipo dentro da condição.
  6. Passo 6 - Conditional aninhado: Dá pra encadear condições: T extends string ? A : T extends number ? B : C. Como if/else if/else pra tipos.

Exemplos Práticos de Conditional Types

Vamos ver código que mostra o poder real dos tipos condicionais.

Conditional Type Básico

// Syntax basica: T extends U ? TrueType : FalseType
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<'hello'>; // true (literal string extends string)

// Retornando tipos diferentes baseado na condicao
type TypeName<T> = 
  T extends string ? 'string' :
  T extends number ? 'number' :
  T extends boolean ? 'boolean' :
  T extends Function ? 'function' :
  'object';

type T1 = TypeName<string>;     // 'string'
type T2 = TypeName<() => void>; // 'function'
type T3 = TypeName<string[]>;   // 'object'

Distribuição: O Superpoder dos Conditional Types

// Quando T e um union, a condicao aplica pra cada membro
type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// Distribui: ToArray<string> | ToArray<number>
// Resultado: string[] | number[]
// NAO e (string | number)[] !!

// Pra desligar distribuicao, envolva com colchetes
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type Result2 = ToArrayNonDist<string | number>;
// Resultado: (string | number)[]

// Filtragem com never - like Exclude
type OnlyStrings<T> = T extends string ? T : never;

type Filtered = OnlyStrings<'a' | 1 | 'b' | true | 'c'>;
// Resultado: 'a' | 'b' | 'c'
// O never some do union automaticamente

Filtrando com Never: Como Exclude e Extract Funcionam

// Recriando Exclude do zero
type MeuExclude<T, U> = T extends U ? never : T;

type Status = 'loading' | 'success' | 'error' | 'idle';
type ActiveStatus = MeuExclude<Status, 'idle'>;
// Resultado: 'loading' | 'success' | 'error'

// Recriando Extract do zero
type MeuExtract<T, U> = T extends U ? T : never;

type Strings = MeuExtract<string | number | boolean, string>;
// Resultado: string

// Recriando NonNullable do zero
type MeuNonNullable<T> = T extends null | undefined ? never : T;

type SafeValue = MeuNonNullable<string | null | undefined>;
// Resultado: string

// Filtrando eventos por tipo
type AppEvent = 
  | { kind: 'click'; x: number }
  | { kind: 'scroll'; offset: number }
  | { kind: 'keypress'; key: string };

type ClickEvent = Extract<AppEvent, { kind: 'click' }>;
// Resultado: { kind: 'click'; x: number }

Infer: Extraindo Tipos Automaticamente

// infer extrai um tipo dentro do conditional

// Pegar tipo dos elementos de um array
type ElementOf<T> = T extends Array<infer U> ? U : never;

type Elem = ElementOf<string[]>;  // string
type Elem2 = ElementOf<number[]>; // number

// Pegar tipo de retorno (como ReturnType funciona)
type MeuReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Ret = MeuReturnType<() => string>; // string
type Ret2 = MeuReturnType<(x: number) => boolean>; // boolean

// Pegar tipo do primeiro parametro
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type P = FirstParam<(name: string, age: number) => void>; // string

// Extrair tipo de uma Promise
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type Val = Awaited<Promise<string>>;           // string
type Val2 = Awaited<Promise<Promise<number>>>; // number (recursivo!)

Exemplo Real: API Response Tipada Condicionalmente

// Cenario real: response de API que muda baseado no endpoint
interface Endpoints {
  '/users': User[];
  '/users/:id': User;
  '/posts': Post[];
  '/settings': Settings;
}

type ApiResponse<T extends keyof Endpoints> = {
  data: Endpoints[T];
  status: number;
  timestamp: string;
};

// O tipo de data muda automaticamente
async function fetchApi<T extends keyof Endpoints>(
  endpoint: T
): Promise<ApiResponse<T>> {
  const res = await fetch(endpoint);
  return res.json();
}

// TypeScript sabe o tipo do retorno
const users = await fetchApi('/users');       // data: User[]
const settings = await fetchApi('/settings'); // data: Settings

Conditional types transformam o sistema de tipos do TypeScript numa linguagem de programação pra tipos. Você consegue expressar lógica complexa que seria impossível só com interfaces e types simples.

Erros Comuns com Conditional Types

Cuidados que evitam horas de debug

Não entender distribuição: quando T é union, o conditional aplica pra cada membro. IsString NÃO é o mesmo que avaliar 'string | number' de uma vez. Se quer desligar distribuição, use [T] extends [U].

Abusar de infer: infer só funciona dentro de conditional types, na posição do branch true. Tentar usar fora de um extends gera erro de compilação.

Recursão infinita: conditional types podem ser recursivos, mas sem caso base o compilador trava. Sempre tenha uma condição de parada clara.

Confundir never em unions: never some automaticamente de unions. Se seu conditional retorna never em todos os branches, o tipo final é never. Isso geralmente indica que nenhum membro do union passou na condição.

Conditional types em genéricos adiados: quando T é um parâmetro genérico não resolvido, o TypeScript NÃO avalia o conditional na hora. Ele adia até T ser conhecido. Isso pode gerar tipos que parecem errados mas estão certos.

Checklist de Conditional Types

  • Syntax T extends U ? X : Y usada corretamente
  • Entende que extends significa 'é atribuível a', não herança
  • Sabe quando a distribuição acontece e como desligar com [T]
  • Usa never pra filtrar membros indesejados de unions
  • Usa infer pra extrair partes de tipos complexos
  • Conditional types recursivos têm caso base definido
  • Testa o resultado com type assertions pra confirmar
  • Não usa any quando unknown resolve o problema

Tipagem Avançada na Prática

Conditional types são o nível que separa quem usa TypeScript de quem domina TypeScript. No CrazyStack, você aplica tipos condicionais em APIs, componentes React e validação de dados. Tudo num projeto real que vai do zero ao deploy.

Se você quer parar de depender de any e começar a criar tipos que se adaptam ao contexto, esse é o próximo passo concreto.

Perguntas frequentes

O Que São Conditional Types

Conditional Types são tipos que mudam dependendo de uma condição. A syntax é parecida com o ternário do JavaScript: T extends U ? X : Y. Se T é compatível com U, o resultado é X. Senão, é Y. O extends aqui NÃO significa herança. Significa 'é atribuível a'. Quando você escreve string extends object, tá perguntando: 'dá pra atribuir string onde espera object?'. A resposta define qual branch do tipo é usado. A parte interessante é a distribuição. Quando o T é um union type, o TypeScript aplica a condição pra cada membro separadamente. Isso é o que torna conditional types tão poderosos pra filtrar e transformar unions.

Como Usar Conditional Types Passo a Passo

Vamos do básico ao avançado. Cada passo constrói em cima do anterior.