Los genéricos son una herramienta de TypeScript que permite escribir funciones, clases e interfaces que trabajan con cualquier tipo sin perder la seguridad de tipado.
En lugar de repetir el mismo código para string, number u otros tipos, escribes una sola versión flexible. TypeScript deduce el tipo correcto en cada uso.
¿Por qué existen los genéricos?
Sin genéricos, tienes dos opciones malas: repetir código o usar any.
Usar any desactiva la verificación de tipos. Eso borra la ventaja principal de TypeScript.
Los genéricos resuelven esto: código flexible y seguro al mismo tiempo.
Sintaxis básica de un genérico
La sintaxis usa corchetes angulares <T> después del nombre de la función o clase.
function identidad<T>(valor: T): T {
return valor;
}
La letra T es un parámetro de tipo. Funciona como una variable, pero para tipos.
Puedes llamarla T, U, V, o cualquier nombre descriptivo como TProducto.
Ejemplos con funciones genéricas
Ejemplo 1 — Función simple
function identidad<T>(valor: T): T {
return valor;
}
const nombre = identidad<string>("Bimbo");
const precio = identidad<number>(18500);
console.log(nombre); // Bimbo
console.log(precio); // 18500
TypeScript sabe que nombre es string y precio es number. No necesitas anotarlos manualmente.
Ejemplo 2 — Función con arreglo
Esta función obtiene el primer elemento de cualquier arreglo.
function primero<T>(lista: T[]): T {
return lista[0];
}
const productos = ["Pan Bimbo", "Marinela", "Ricolino"];
const precios = [12500, 18500, 9800];
console.log(primero(productos)); // Pan Bimbo
console.log(primero(precios)); // 12,500
La misma función trabaja con string[] y con number[]. TypeScript infiere el tipo automáticamente.
Ejemplo 3 — Función con dos parámetros de tipo
Puedes usar más de un parámetro de tipo. Aquí construimos un par clave-valor.
function crearPar<K, V>(clave: K, valor: V): [K, V] {
return [clave, valor];
}
const parProducto = crearPar<string, number>("Precio OXXO", 28500);
console.log(parProducto); // ["Precio OXXO", 28500]
const parActivo = crearPar<string, boolean>("Cuenta activa", true);
console.log(parActivo); // ["Cuenta activa", true]
K y V son convenciones para "Key" y "Value". Son solo nombres; puedes elegir otros.
Genéricos con interfaces
Puedes aplicar genéricos a interfaces para describir estructuras de datos flexibles.
interface Respuesta<T> {
exito: boolean;
datos: T;
mensaje: string;
}
Esta interface representa cualquier respuesta de una API. El campo datos puede ser cualquier tipo.
interface Producto {
id: number;
nombre: string;
precio: number;
}
const respuestaProducto: Respuesta<Producto> = {
exito: true,
datos: { id: 1, nombre: "Despensa Bimbo", precio: 12500 },
mensaje: "Producto encontrado"
};
const respuestaLista: Respuesta<string[]> = {
exito: true,
datos: ["FEMSA", "Liverpool", "Bimbo"],
mensaje: "Lista de empresas"
};
console.log(respuestaProducto.datos.nombre); // Despensa Bimbo
console.log(respuestaLista.datos[1]); // Liverpool
Reutilizas Respuesta<T> para cualquier tipo de dato que regrese tu API.
Genéricos con clases
Las clases también aceptan parámetros de tipo. Esto es útil para estructuras como pilas, colas o repositorios.
Ejemplo — Clase Almacén genérico
class Almacen<T> {
private items: T[] = [];
agregar(item: T): void {
this.items.push(item);
}
obtener(indice: number): T {
return this.items[indice];
}
total(): number {
return this.items.length;
}
}
Ahora usamos la clase con tipos concretos:
const almacenProductos = new Almacen<string>();
almacenProductos.agregar("Tequila FEMSA");
almacenProductos.agregar("Cerveza Sol");
console.log(almacenProductos.obtener(0)); // Tequila FEMSA
console.log(almacenProductos.total()); // 2
const almacenPrecios = new Almacen<number>();
almacenPrecios.agregar(18500);
almacenPrecios.agregar(9800);
console.log(almacenPrecios.obtener(1)); // 9,800
TypeScript garantiza que almacenProductos solo acepta string y almacenPrecios solo acepta number.
Restricciones de tipo con extends
A veces necesitas limitar qué tipos puede recibir un genérico. Usas la palabra clave extends.
interface ConNombre {
nombre: string;
}
function mostrarNombre<T extends ConNombre>(item: T): string {
return item.nombre;
}
Esta función solo acepta tipos que tengan la propiedad nombre.
const empresa = { nombre: "Mercado Libre", pais: "México" };
const empleado = { nombre: "Carlos", puesto: "Dev", salario: 28000 };
console.log(mostrarNombre(empresa)); // Mercado Libre
console.log(mostrarNombre(empleado)); // Carlos
// Esto causa error en tiempo de compilación:
// mostrarNombre(12500); ❌ number no tiene 'nombre'
Las restricciones hacen los genéricos más seguros sin quitarles flexibilidad.
Valor por defecto en genéricos
Desde TypeScript 2.3, puedes asignar un tipo por defecto a un parámetro genérico.
interface Contenedor<T = string> {
valor: T;
}
const a: Contenedor = { valor: "OXXO" }; // T es string por defecto
const b: Contenedor<number> = { valor: 4500 }; // T es number explícito
Esto reduce la verbosidad cuando el tipo más común ya está definido.
Errores comunes
Error 1 — Usar any en lugar de un genérico
// ❌ Incorrecto: pierdes el tipado
function obtenerValor(lista: any[]): any {
return lista[0];
}
// ✅ Correcto: mantienes el tipado
function obtenerValor<T>(lista: T[]): T {
return lista[0];
}
Con any, TypeScript no detecta errores. Con <T>, sí.
Error 2 — Asumir propiedades que el tipo genérico no tiene
// ❌ Incorrecto: T puede no tener 'precio'
function descuento<T>(item: T): number {
return item.precio * 0.9; // Error: Property 'precio' does not exist on type 'T'
}
// ✅ Correcto: restringe T para garantizar la propiedad
interface ConPrecio {
precio: number;
}
function descuento<T extends ConPrecio>(item: T): number {
return item.precio * 0.9;
}
Siempre usa extends cuando necesites acceder a propiedades específicas del tipo.
Error 3 — Olvidar el parámetro de tipo en clases
// ❌ Sin tipo, TypeScript infiere 'unknown' o lanza error
const caja = new Almacen();
caja.agregar("Producto"); // Puede fallar según la configuración
// ✅ Especifica el tipo al crear la instancia
const caja = new Almacen<string>();
caja.agregar("Producto");
Siempre indica el tipo cuando creas instancias de clases genéricas.
Tabla de referencia rápida
| Sintaxis | Descripción | Ejemplo |
|---|---|---|
function f<T>(x: T): T |
Función genérica básica | identidad<string>("Bimbo") |
<T, U> |
Múltiples parámetros de tipo | crearPar<string, number>(...) |
interface I<T> |
Interface genérica | Respuesta<Producto> |
class C<T> |
Clase genérica | Almacen<string> |
T extends X |
Restricción de tipo | T extends ConNombre |
T = string |
Tipo por defecto | Contenedor<T = string> |
Cuándo usar genéricos
Usa genéricos cuando tu función, clase o interface trabaja con distintos tipos pero con la misma lógica.
No uses genéricos si la función siempre recibe el mismo tipo. En ese caso, anota el tipo directamente.
La regla práctica: si copiarías y pegarías código solo para cambiar el tipo, usa un genérico.