TypeScript de Cero a Experto / Genéricos
Genéricos desde cero: código que se adapta
Aquí empieza la magia mayor de TypeScript. Un genérico es una función o tipo que
funciona con CUALQUIER tipo sin renunciar a la seguridad —recibe un número y sabe
que devuelve un número; recibe un Gasto y sabe que devuelve un Gasto. Es lo
que hace reutilizable tu código sin caer en any. Suena abstracto; en una función
de tres líneas lo verás clarísimo.
El molde que se adapta al contenido
El problema: reutilizar sin perder el tipo
Imagina una función que devuelve el primer elemento de un array. Sin genéricos, tienes dos opciones malas:
// opción mala 1: una función por tipo (repetición infinita)
function primerNumero(arr: number[]): number { return arr[0]; }
function primerTexto(arr: string[]): string { return arr[0]; }
// ...¿y para Gasto? ¿y para Usuario? insostenible
// opción mala 2: any (funciona con todo, pero BORRA el tipo)
function primeroAny(arr: any[]): any { return arr[0]; }
const x = primeroAny([1, 2, 3]); // x es 'any' → perdiste que era number
x.toUpperCase(); // TS no se queja... 💥 revienta en runtimeLa opción 1 te obliga a escribir la misma función para cada tipo. La opción 2
funciona con todos, pero devuelve any —y ya sabes (módulo 2) que any apaga la
seguridad: el resultado pierde su tipo y TS deja de protegerte. Necesitas algo que
funcione con cualquier tipo Y lo conserve. Eso es un genérico.
Tu primer genérico: identidad<T>
Un genérico introduce una VARIABLE DE TIPO —por convención T— entre <>. Es un
"tipo por determinar" que TS rellena según cómo uses la función:
// <T> declara una variable de tipo; T se usa como cualquier tipo
function identidad<T>(valor: T): T {
return valor;
}
// se lee: "para algún tipo T, recibe un T y devuelve un T"
// TS INFIERE T según lo que pases —no tienes que escribirlo:
const a = identidad("hola"); // T = string → a es string
const b = identidad(42); // T = number → b es number
const c = identidad(true); // T = boolean → c es boolean
a.toUpperCase(); // ✓ TS sabe que a es string
b.toFixed(2); // ✓ TS sabe que b es number
// b.toUpperCase(); // ❌ TS lo marca: b es number, no stringCompara con any: identidad(42) devuelve number (no any), así que TS sigue
protegiéndote. La <T> es la etiqueta de la máquina: se adapta a lo que entra y lo
recuerda a la salida. Y lo mejor —no escribes identidad<string>("hola"): TS
INFIERE que T es string por el argumento. Los genéricos casi siempre se
infieren solos.
El primero de un array, genérico
El caso real del principio, ahora resuelto de una vez para todos los tipos:
// funciona con arrays de CUALQUIER tipo, y conserva el tipo del elemento:
function primero<T>(arr: T[]): T | undefined {
return arr[0]; // undefined si el array está vacío (strict, módulo 1)
}
const n = primero([10, 20, 30]); // T = number → n es number | undefined
const s = primero(["a", "b"]); // T = string → s es string | undefined
const g = primero([{ nombre: "Café" }]); // T = { nombre: string } → g tipado
// una sola función, todos los tipos, cero any, seguridad total:
if (n !== undefined) console.log(n.toFixed(2));¿Cuál es la ventaja de function primero<T>(arr: T[]): T frente a function primero(arr: any[]): any?
Mini-reto
Genera tu primer genérico útil: 1) escribe function pares<T>(a: T, b: T): [T, T]
que devuelva una tupla con ambos valores del MISMO tipo; 2) pruébala con dos
números y con dos strings, y confirma que TS rechaza pares(1, "a") (tipos
distintos para el mismo T); 3) escribe function repetir<T>(valor: T, veces: number): T[] que devuelva un array con valor repetido. Sientes cómo un solo T
ata los tipos entre parámetros y retorno.
Qué sigue
Un genérico simple ya es poderoso. La próxima lección sube la apuesta: varios
parámetros de tipo a la vez (<T, U>), para funciones que relacionan DOS tipos
—como un map que transforma de un tipo a otro, o pares clave-valor. La caja de
herramientas genérica se agranda.