Los genéricos en TypeScript son tipos que se dejan en blanco y se rellenan al usarlos. Sirven para escribir una función o una clase una sola vez y que funcione con números, textos o tus propios objetos sin perder el tipo por el camino. Cuatro ejemplos, de lo más simple a una clase completa.
También te puede interesar
El problema que resuelven
Una función que devuelve el primer elemento de una lista debería servir para cualquier lista. Si la declaramos con unknown[], sirve, pero lo que devuelve es unknown y no se puede usar:
function primeroDesconocido(lista: unknown[]): unknown {
return lista[0];
}
const d = primeroDesconocido([3, 1, 2]);
console.log(d.toFixed(2));npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es desconocido.ts
tscdesconocido.ts(5,13): error TS18046: "d" es de tipo "unknown".
Una función genérica
Con <T> le decimos: «los elementos son de un tipo T, y devuelvo uno de ese mismo tipo». TypeScript deduce T en cada llamada:
// Sin genéricos: o una función por tipo, o perder el tipo con unknown.
function primeroDesconocido(lista: unknown[]): unknown {
return lista[0];
}
// Con un genérico: T es «el tipo que tengan los elementos», sea cual sea.
function primero<T>(lista: T[]): T | undefined {
return lista[0];
}
const n = primero([3, 1, 2]); // T = number
const s = primero(["ana", "luis"]); // T = string
const vacio = primero<boolean>([]); // T indicado a mano
console.log(n, s, vacio);
console.log(n?.toFixed(2), s?.toUpperCase());
const d = primeroDesconocido([3, 1, 2]);
console.log(typeof d);Ábrelo en el editor y cámbialo →
npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es basicos.ts
node basicos.ts
salida3 ana undefined 3.00 ANA number
Fíjate en T | undefined: si la lista está vacía, lista[0] no existe. Por eso luego se usa n?.toFixed(2). El genérico no esconde ese caso.
Dos parámetros de tipo: agrupar
Agrupar una lista por una clave necesita dos tipos: el de los elementos (T) y el de la clave (K). K extends PropertyKey exige que la clave pueda usarse como nombre de propiedad: un texto, un número o un símbolo.
// Dos parámetros de tipo: los elementos (T) y la clave por la que se agrupa (K).
function agrupar<T, K extends PropertyKey>(lista: T[], clave: (x: T) => K): Record<K, T[]> {
const grupos = {} as Record<K, T[]>;
for (const x of lista) {
const k = clave(x);
(grupos[k] ??= []).push(x);
}
return grupos;
}
interface Producto {
nombre: string;
categoria: "audio" | "video" | "accesorios";
precio: number;
}
const productos: Producto[] = [
{ nombre: "Audífonos", categoria: "audio", precio: 1299.99 },
{ nombre: "Webcam", categoria: "video", precio: 899 },
{ nombre: "Bocina", categoria: "audio", precio: 649.5 },
{ nombre: "Cable HDMI", categoria: "accesorios", precio: 189 },
];
const porCategoria = agrupar(productos, (p) => p.categoria);
// porCategoria: Record<"audio" | "video" | "accesorios", Producto[]>
for (const [cat, lista] of Object.entries(porCategoria)) {
console.log(cat, lista.map((p) => p.nombre).join(", "));
}
const porRango = agrupar(productos, (p) => (p.precio >= 500 ? "caros" : "baratos"));
console.log(Object.keys(porRango));Ábrelo en el editor y cámbialo →
npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es agrupar.ts
node agrupar.ts
salidaaudio Audífonos, Bocina video Webcam accesorios Cable HDMI [ 'caros', 'baratos' ]
En la primera llamada, K es "audio" | "video" | "accesorios", porque eso devuelve la función de la clave. En la segunda, "caros" | "baratos". El resultado sabe exactamente qué grupos puede haber.
Restricciones: «cualquier tipo, siempre que…»
Con extends se pone una condición. porId acepta listas de lo que sea, pero cada elemento tiene que tener un id numérico. Y como T es el tipo completo, el resultado conserva todas sus propiedades, como el correo:
// T puede ser cualquier tipo... siempre que tenga un id numérico.
function porId<T extends { id: number }>(lista: T[], id: number): T | undefined {
return lista.find((x) => x.id === id);
}
const usuarios = [
{ id: 1, nombre: "Ana", correo: "ana@ejemplo.com" },
{ id: 2, nombre: "Luis", correo: "luis@ejemplo.com" },
];
const u = porId(usuarios, 2);
console.log(u?.correo); // el resultado conserva TODAS las propiedades
const etiquetas = [{ clave: "a" }, { clave: "b" }];
porId(etiquetas, 1);Ábrelo en el editor y cámbialo →
npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es restriccion.ts
tscrestriccion.ts(15,7): error TS2345: No se puede asignar un argumento de tipo "{ clave: string; }[]" al parámetro de tipo "{ id: number; }[]".
La propiedad "id" falta en el tipo "{ clave: string; }", pero es obligatoria en el tipo "{ id: number; }".La última línea falla a propósito: las etiquetas no tienen id.
Una clase genérica: una caché con caducidad
Las clases también pueden ser genéricas. Esta caché guarda valores de un tipo V durante un tiempo; new Cache<number> crea una que solo admite números. La última línea intenta guardar un texto:
// Una clase genérica: una caché con caducidad para valores de cualquier tipo.
class Cache<V> {
#datos = new Map<string, { valor: V; caduca: number }>();
readonly duracionMs: number;
constructor(duracionMs: number) {
this.duracionMs = duracionMs;
}
guardar(clave: string, valor: V): void {
this.#datos.set(clave, { valor, caduca: Date.now() + this.duracionMs });
}
leer(clave: string): V | undefined {
const e = this.#datos.get(clave);
if (!e) return undefined;
if (Date.now() > e.caduca) {
this.#datos.delete(clave);
return undefined;
}
return e.valor;
}
}
const precios = new Cache<number>(50);
precios.guardar("webcam", 899);
console.log("al momento:", precios.leer("webcam"));
await new Promise((r) => setTimeout(r, 80));
console.log("a los 80 ms:", precios.leer("webcam"));
precios.guardar("bocina", "649.50");Ábrelo en el editor y cámbialo →
npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es cache.ts
tsccache.ts(31,27): error TS2345: No se puede asignar un argumento de tipo "string" al parámetro de tipo "number".
Sin esa última línea, compila y funciona así:
salidaal momento: 899 a los 80 ms: undefined
<T> que solo aparece una vez en la firma no relaciona nada con nada.Todo el código de esta página se comprobó con TypeScript 7.0.2 en modo estricto (--strict --locale es) y se ejecutó con Node.js 24.22 en un contenedor limpio antes de publicarla; los mensajes de error y las salidas están copiados de esa ejecución. Documentación oficial: Generics en el manual de TypeScript.