¿Qué son los generics en TypeScript y para qué sirven?
Ver respuesta — intenta responderla en voz alta primero
Los generics son parámetros de tipo: permiten escribir funciones y estructuras que trabajan con varios tipos sin perder la relación entre la entrada y la salida. Si a primerElemento le pasas un string[], el compilador sabe que devuelve string, no un genérico any.
Imagina una función que devuelve el primer elemento de un arreglo. Sin generics tienes dos malas opciones: tiparla con any (pierdes toda la seguridad) o escribir una versión por cada tipo (duplicas código). Los generics dan la tercera vía:
<T>es un parámetro de tipo: como los parámetros normales, pero de tipos. Se "rellena" en cada llamada, casi siempre por inferencia.- Conservan la relación entrada → salida:
function primero<T>(arr: T[]): Tpromete que devuelve el mismo tipo que contiene el arreglo. - Restricciones con
extends:<T extends { id: number }>significa "cualquier tipo, siempre que tenga unidnumérico". Dentro de la función puedes usar.idcon seguridad. K extends keyof T: el patrón estrella.Ksolo puede ser una clave real deT, así que acceder aobjeto[clave]es seguro y el retorno queda tipado comoT[K].
En React los usas sin darte cuenta: useState<number>(0) es un hook genérico.
// Sin generics se pierde el tipo:
// function primerElemento(arr: any[]): any -> la salida es any
// Con generics, la relación entrada -> salida se conserva
function primerElemento<T>(arr: T[]): T | undefined {
return arr[0];
}
const numero = primerElemento([10, 20, 30]); // number | undefined
const palabra = primerElemento(["a", "b", "c"]); // string | undefined
// numero.toUpperCase();
// Error: Property 'toUpperCase' does not exist on type 'number'.
// Restricción con extends: T debe tener longitud
function masLargo<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
masLargo("hola", "adiós"); // funciona: string tiene length
masLargo([1, 2], [1, 2, 3]); // funciona: los arreglos tienen length
// masLargo(10, 20);
// Error: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
// K extends keyof T: solo acepta claves reales del objeto
function extraerPropiedad<T, K extends keyof T>(objeto: T, clave: K): T[K] {
return objeto[clave];
}
const usuario = { nombre: "Ana", edad: 30 };
const nombre = extraerPropiedad(usuario, "nombre"); // string
const edad = extraerPropiedad(usuario, "edad"); // number
// extraerPropiedad(usuario, "email");
// Error: Argument of type '"email"' is not assignable to parameter of type '"nombre" | "edad"'.
Usar any donde iba un generic, "porque total, funciona con todo":
// Compila, pero la salida pierde el tipo
function primeroMal(arr: any[]): any {
return arr[0];
}
const x = primeroMal([1, 2, 3]);
x.toUpperCase(); // compila y explota en runtime: x es any
// Corrección: el generic conserva el tipo
function primeroBien<T>(arr: T[]): T | undefined {
return arr[0];
}
const y = primeroBien([1, 2, 3]); // number | undefined
La diferencia clave: any acepta todo y olvida todo; el generic acepta todo y recuerda el tipo.
"Un generic es un parámetro de tipo: me deja escribir una sola función que funciona con muchos tipos sin perder la relación entre entrada y salida. Por ejemplo,
primerElemento<T>(arr: T[]): Tdevuelve number si le paso números y string si le paso strings, cosa que conanyperdería. Puedo restringir el generic conextends, y el patrón que más uso esK extends keyof Tpara acceder a propiedades de un objeto de forma segura. En React los uso a diario:useState<T>es un hook genérico."
Escribe ultimoElemento<T> que devuelva el último elemento de un arreglo (o undefined si está vacío), y comprueba que con ["a", "b"] el resultado queda tipado como string | undefined.
Ver solución
function ultimoElemento<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
const letra = ultimoElemento(["a", "b"]); // string | undefined
const numero = ultimoElemento([1, 2, 3]); // number | undefined
const vacio = ultimoElemento<number>([]); // undefined en runtime
// La relación se conserva: si letra existe, es un string
if (letra !== undefined) {
console.log(letra.toUpperCase()); // "B"
}