Introdução: a magia que está oculta nos tipos
\n\nImagine que você não está apenas escrevendo código, mas um conjunto de instruções para o próprio compilador, que com base nas suas regras gera construções seguras e autodocumentadas. Isso é a metaprogramação em TypeScript — a capacidade de escrever código que opera no nível dos tipos, analisa e transforma outros tipos durante a compilação.
\n\nMuitos desenvolvedores param nos genéricos e interfaces básicos, sem sequer suspeitar do poderoso arsenal escondido por trás das palavras-chave infer, extends e construções condicionais. Neste artigo, exploraremos técnicas avançadas de metaprogramação TypeScript que o transformarão de um simples usuário da linguagem em um verdadeiro arquiteto dela.
1. Tipos condicionais: if no nível dos tipos
\n\nTipos condicionais são a base de qualquer programação em nível de tipo. Eles funcionam como um operador ternário, mas no nível do sistema de tipos:
\n\ntype IsString<T> = T extends string ? "yes" : "no";\n\ntype Test1 = IsString<"hello">; // "yes"\ntype Test2 = IsString<42>; // "no"\n\n\nMas a verdadeira magia começa quando os combinamos com genéricos. Por exemplo, vamos criar um tipo que extrai apenas as chaves de string de um objeto:
\n\ntype StringKeys<T> = {\n [K in keyof T]: T[K] extends string ? K : never\n}[keyof T];\n\ninterface User {\n name: string;\n age: number;\n email: string;\n}\n\ntype UserStringKeys = StringKeys<User>; // "name" | "email"\n\n\nEste exemplo mostra como tipos avançados TypeScript permitem filtrar e transformar estruturas de dados em tempo de compilação. Você está literalmente programando a lógica que é executada antes do código chegar ao runtime.
\n\n2. Infer: extraindo tipos de outros tipos
\n\nA palavra-chave infer é, talvez, a ferramenta mais poderosa no arsenal da metaprogramação TypeScript. Ela permite "olhar para dentro" de um tipo e extrair suas partes constituintes. Vejamos um exemplo clássico — obter o tipo de retorno de uma função:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;\n\ntype Fn = (x: number) => string;\ntype FnReturn = ReturnType<Fn>; // string\n\n\nMas o infer pode fazer muito mais. Vamos extrair o tipo do primeiro argumento de uma função:
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;\n\ntype Handler = (id: number, name: string) => void;\ntype HandlerFirstArg = FirstArg<Handler>; // number\n\n\nE que tal uma análise recursiva de Promises? Vamos criar um tipo que "desembrulha" todos os Promises aninhados:
\n\ntype UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;\n\ntype DeepPromise = Promise<Promise<Promise<number>>>;\ntype Unwrapped = UnwrapPromise<DeepPromise>; // number\n\n\nObserve a recursão — esta é uma das técnicas avançadas TypeScript mais elegantes, que permite lidar com estruturas infinitamente aninhadas.
\n\n3. Mapped types: transformação de objetos
\n\nMapped types permitem criar novos tipos com base em tipos existentes, iterando sobre suas chaves. É uma ferramenta indispensável para criar utilitários genéricos:
\n\ntype Readonly<T> = {\n readonly [K in keyof T]: T[K];\n};\n\ntype Partial<T> = {\n [K in keyof T]?: T[K];\n};\n\n\nMas podemos ir além. Vamos criar um tipo que torna todas as propriedades opcionais, mas apenas se forem strings:
\n\ntype PartialStringProps<T> = {\n [K in keyof T]: T[K] extends string ? T[K] | undefined : T[K];\n};\n\ninterface Config {\n nam