Como Usar Tipos Condicionais em TypeScript: infer, extends e Mapped Types em Produção Já leu

Entendendo o Sistema de Tipos Avançado do TypeScript O TypeScript oferece um sistema de tipos extremamente poderoso que vai muito além de tipos simples como e . Quando você domina conceitos como , e , consegue criar abstrações sofisticadas que tornam seu código mais seguro, reutilizável e expressivo. Este artigo é uma jornada prática por esses três pilares fundamentais do TypeScript avançado. Estes conceitos não são apenas acadêmicos — grandes frameworks como React, Vue e bibliotecas utilitárias dependem deles. Uma vez que você os compreender, verá oportunidades de melhorar drasticamente a qualidade do seu código TypeScript. Extends: O Fundamento das Restrições de Tipo O em TypeScript é um operador de restrição que permite você verificar se um tipo é compatível com outro. Diferente da herança em classes, aqui estamos no universo dos tipos, não de instâncias. Use para criar condicionais de tipo e limitar o que pode ser passado como genérico. A real força do emerge quando combinado com types

Entendendo o Sistema de Tipos Avançado do TypeScript

O TypeScript oferece um sistema de tipos extremamente poderoso que vai muito além de tipos simples como string e number. Quando você domina conceitos como infer, extends e Mapped Types, consegue criar abstrações sofisticadas que tornam seu código mais seguro, reutilizável e expressivo. Este artigo é uma jornada prática por esses três pilares fundamentais do TypeScript avançado.

Estes conceitos não são apenas acadêmicos — grandes frameworks como React, Vue e bibliotecas utilitárias dependem deles. Uma vez que você os compreender, verá oportunidades de melhorar drasticamente a qualidade do seu código TypeScript.

Extends: O Fundamento das Restrições de Tipo

O extends em TypeScript é um operador de restrição que permite você verificar se um tipo é compatível com outro. Diferente da herança em classes, aqui estamos no universo dos tipos, não de instâncias. Use extends para criar condicionais de tipo e limitar o que pode ser passado como genérico.

// Exemplo básico: restrição simples
function processar<T extends string | number>(valor: T): T {
  return valor;
}

processar("texto"); // ✓ Funciona
processar(123); // ✓ Funciona
processar(true); // ✗ Erro: boolean não estende string | number

// Exemplo prático: validação de chaves de objeto
type User = { name: string; age: number; email: string };

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { name: "Ana", age: 28, email: "ana@email.com" };
const nome = getProperty(user, "name"); // ✓ "name" é chave válida
getProperty(user, "telefone"); // ✗ Erro: "telefone" não existe em User

A real força do extends emerge quando combinado com types condicionais. Use a sintaxe T extends U ? X : Y para criar ramificações lógicas nos tipos. Isso permite que você escreva tipos que se comportam diferentemente dependendo de suas entradas.

// Tipo condicional prático
type IsString<T> = T extends string ? true : false;

type A = IsString<"olá">; // true
type B = IsString<number>; // false

// Exemplo real: extrair tipo de array
type ArrayElement<T> = T extends (infer E)[] ? E : never;

type NumArray = ArrayElement<number[]>; // number
type StrArray = ArrayElement<string[]>; // string
type NotArray = ArrayElement<boolean>; // never

Infer: Capturando Tipos Desconhecidos

O infer é uma palavra-chave mágica que permite você extrair e capturar tipos de estruturas complexas sem conhecê-los antecipadamente. Ele só funciona dentro de um extends e marca um local onde TypeScript deve "adivinhar" qual é o tipo.

Imagine que você recebe um tipo complexo e precisa extrair um pedaço específico dele. É exatamente para isso que infer existe. A sintaxe é simples: quando TypeScript vê infer T, ele captura aquele tipo em uma variável T que você pode usar depois.

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

type ResultA = Awaited<Promise<string>>; // string
type ResultB = Awaited<number>; // number

// Exemplo 2: Extrair tipos de função
type FunctionReturn<T> = T extends (...args: any[]) => infer R ? R : never;

function saudar(nome: string): string {
  return `Olá, ${nome}!`;
}

type SaudacaoReturn = FunctionReturn<typeof saudar>; // string

// Exemplo 3: Extrair parâmetros de função
type FunctionParams<T> = T extends (...args: infer P) => any ? P : never;

type SaudacaoParams = FunctionParams<typeof saudar>; // [nome: string]

A verdadeira beleza do infer aparece quando você processa tipos recursivamente ou trabalha com estruturas aninhadas. É a ferramenta que permite criar utilitários tipo-seguros para bibliotecas e frameworks complexos.

Mapped Types: Transformando Estruturas de Tipos

Um Mapped Type é um tipo que itera sobre as propriedades de outro tipo e cria um novo tipo transformado. Use a sintaxe { [K in Keys]: T } para mapear sobre chaves e gerar novas propriedades. Isso é especialmente útil para criar variações de tipos existentes.

// Exemplo 1: Tornar todas as propriedades opcionais
type Partial<T> = {
  [K in keyof T]?: T[K];
};

type User = { name: string; age: number };
type OptionalUser = Partial<User>;
// Resultado: { name?: string; age?: number }

// Exemplo 2: Criar versão "readonly" de um tipo
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type ImmutableUser = Readonly<User>;
// Resultado: { readonly name: string; readonly age: number }

// Exemplo 3: Converter todas as propriedades em getters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// Resultado: { getName: () => string; getAge: () => number }

Combine Mapped Types com tipos condicionais e infer para criar transformações sofisticadas. Isso é o que permite que bibliotecas como ts-jest e ferramentas de validação ofereçam suporte tipo-seguro tão impressionante.

// Exemplo avançado: Filtrar propriedades por tipo
type PropertiesOfType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

type Product = { name: string; price: number; active: boolean };
type StringProps = PropertiesOfType<Product, string>; // { name: string }
type NumberProps = PropertiesOfType<Product, number>; // { price: number }

Conclusão

Três aprendizados essenciais consolidam sua maestria em tipos condicionais avançados: Primeiro, extends é seu validador — use-o para restringir genéricos e criar condicionais de tipo que bifurcam a lógica baseado em compatibilidade. Segundo, infer é seu extrator — permite capturar tipos de estruturas complexas sem conhecê-los explicitamente, sendo indispensável para tipos como Awaited e ReturnType. Terceiro, Mapped Types são seus transformadores — iteram sobre tipos existentes e geram novos tipos derivados, mantendo segurança em larga escala.

Domine esses três conceitos em conjunto e você terá as ferramentas para construir sistemas de tipos robustos, reutilizáveis e que escalam com seu projeto.

Referências


Artigos relacionados