Introducción: la magia que se esconde en los tipos
\n\nImagina que no solo escribes código, sino un conjunto de instrucciones para el propio compilador que, basándose en tus reglas, genera construcciones seguras y autodocumentadas. Esto es la metaprogramación en TypeScript: la capacidad de escribir código que funciona a nivel de tipos, analiza y transforma otros tipos durante la compilación.
\n\nMuchos desarrolladores se quedan en los genéricos e interfaces básicos, sin siquiera sospechar el poderoso arsenal que se esconde detrás de las palabras clave infer, extends y las construcciones condicionales. En este artículo, analizaremos técnicas avanzadas de metaprogramación en TypeScript que te transformarán de un simple usuario del lenguaje a un verdadero arquitecto del mismo.
1. Tipos condicionales: if a nivel de tipos
\n\nLos tipos condicionales son la base de cualquier programación a nivel de tipos. Funcionan como un operador ternario, pero a nivel del 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\nPero la verdadera magia comienza cuando los combinamos con genéricos. Por ejemplo, creemos un tipo que extrae solo las claves de tipo string de un 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 ejemplo muestra cómo los tipos avanzados de TypeScript permiten filtrar y transformar estructuras de datos en tiempo de compilación. Literalmente estás programando la lógica que se ejecuta antes de que el código llegue al runtime.
\n\n2. Infer: extracción de tipos a partir de otros tipos
\n\nLa palabra clave infer es, quizás, la herramienta más poderosa en el arsenal de la metaprogramación en TypeScript. Permite "mirar dentro" de un tipo y extraer sus partes constituyentes. Veamos un ejemplo clásico: obtener el tipo de retorno de una función:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;\n\ntype Fn = (x: number) => string;\ntype FnReturn = ReturnType<Fn>; // string\n\n\nPero infer puede hacer mucho más. Extraigamos el tipo del primer argumento de una función:
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\n¿Y qué tal un análisis recursivo de promesas? Creemos un tipo que "desenvuelva" todas las Promises anidadas:
\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\nObserva la recursividad: esta es una de las técnicas avanzadas de TypeScript más elegantes, que permite manejar estructuras infinitamente anidadas.
\n\n3. Mapped types: transformación de objetos
\n\nLos Mapped types permiten crear nuevos tipos basados en tipos existentes, iterando sobre sus claves. Son una herramienta indispensable para crear utilidades genéricas:
\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\nPero podemos ir más allá. Creemos un tipo que haga todas las propiedades opcionales, pero solo si son de tipo string:
\n\ntype PartialStringProps<T> = {\n [K in keyof T]: T[K] extends string ? T[K] | undefined : T[K];\n};\n\ninterface Config {\n nam