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.