En esta página
Genéricos desde cero
Genéricos desde cero
Los genéricos son la característica más potente del sistema de tipos de TypeScript. Sin ellos, estarías obligado a elegir entre la seguridad de tipos (escribir funciones separadas para cada tipo) y la reutilización (usar unknown o any y perder información). Los genéricos te dan ambas cosas: código reutilizable que mantiene toda la información de tipos.
El problema: la función identidad
Imagina una función que simplemente devuelve lo que recibe:
// ❌ Solución con any: pierde toda información de tipo
function identidad(valor: any): any {
return valor;
}
const resultado = identidad(42);
resultado.toUpperCase(); // No hay error en compilación... pero falla en runtime// ❌ Solución con sobrecargas: verbosa, no escala
function identidad(valor: string): string;
function identidad(valor: number): number;
function identidad(valor: boolean): boolean;
// ... una sobrecarga por cada tipo posible// ✅ Solución con genéricos: reutilizable y seguro
function identidad<T>(valor: T): T {
return valor;
}
const num = identidad(42); // T = number, resultado: number
const str = identidad('hola'); // T = string, resultado: string
const arr = identidad([1, 2]); // T = number[], resultado: number[]
num.toFixed(2); // ✅ TypeScript sabe que es number
str.toUpperCase(); // ✅ TypeScript sabe que es string
arr.length; // ✅ TypeScript sabe que es number[]El parámetro de tipo <T> es una variable que TypeScript resuelve en el momento en que se llama la función, basándose en el tipo del argumento.
Sintaxis de los genéricos
Los genéricos se declaran entre corchetes angulares <> después del nombre de la función, interfaz, type alias o clase:
// Función genérica
function primero<T>(arreglo: T[]): T | undefined {
return arreglo[0];
}
// Tipo genérico
type Par<T, U> = { primero: T; segundo: U };
// Interfaz genérica
interface Contenedor<T> {
valor: T;
transformar<U>(fn: (valor: T) => U): Contenedor<U>;
}
// Clase genérica
class Caja<T> {
constructor(private contenido: T) {}
obtener(): T { return this.contenido; }
}Inferencia de tipos genéricos
TypeScript es muy bueno infiriendo el tipo genérico a partir de los argumentos. En la mayoría de los casos no necesitas especificarlo explícitamente:
function envolver<T>(valor: T): { valor: T } {
return { valor };
}
// Inferencia automática:
const a = envolver(42); // T = number, tipo: { valor: number }
const b = envolver('texto'); // T = string, tipo: { valor: string }
const c = envolver(true); // T = boolean, tipo: { valor: boolean }
// Especificación explícita (cuando quieres ser más restrictivo):
const d = envolver<string>('texto'); // T explícitamente string
const e = envolver<string | null>(null); // T = string | nullFunciones genéricas prácticas
Transformaciones tipadas
function mapear<T, U>(arreglo: T[], transformar: (item: T) => U): U[] {
return arreglo.map(transformar);
}
const nombres = mapear([1, 2, 3], n => `Item ${n}`);
// nombres: string[] — TypeScript infiere T=number, U=string
const longitudes = mapear(['hola', 'mundo', '!'], s => s.length);
// longitudes: number[]Filtrado tipado con type predicates
function filtrar<T>(arreglo: T[], predicado: (item: T) => boolean): T[] {
return arreglo.filter(predicado);
}
// Con type predicate para narrowing:
function filtrarPorTipo<T, U extends T>(
arreglo: T[],
predicado: (item: T) => item is U
): U[] {
return arreglo.filter(predicado) as U[];
}
type Fruta = { tipo: 'fruta'; nombre: string };
type Verdura = { tipo: 'verdura'; nombre: string };
type Alimento = Fruta | Verdura;
const alimentos: Alimento[] = [
{ tipo: 'fruta', nombre: 'Manzana' },
{ tipo: 'verdura', nombre: 'Zanahoria' },
{ tipo: 'fruta', nombre: 'Naranja' },
];
const esFruta = (a: Alimento): a is Fruta => a.tipo === 'fruta';
const frutas = filtrarPorTipo(alimentos, esFruta);
// frutas: Fruta[]Utilidades de objeto genéricas
// Omitir propiedades de forma segura
function omitir<T, K extends keyof T>(objeto: T, ...claves: K[]): Omit<T, K> {
const resultado = { ...objeto };
claves.forEach(clave => delete resultado[clave]);
return resultado as Omit<T, K>;
}
// Seleccionar propiedades
function seleccionar<T, K extends keyof T>(objeto: T, ...claves: K[]): Pick<T, K> {
const resultado = {} as Pick<T, K>;
claves.forEach(clave => { resultado[clave] = objeto[clave]; });
return resultado;
}
interface Perfil {
id: string;
nombre: string;
email: string;
contraseña: string;
rol: string;
}
const perfil: Perfil = {
id: 'u1', nombre: 'Ana', email: '[email protected]',
contraseña: 'hash123', rol: 'admin'
};
const perfilSeguro = omitir(perfil, 'contraseña');
// tipo: Omit<Perfil, 'contraseña'> — sin contraseña
const perfilMinimo = seleccionar(perfil, 'id', 'nombre');
// tipo: Pick<Perfil, 'id' | 'nombre'>Interfaces genéricas
Las interfaces genéricas son fundamentales para modelar estructuras de datos y contratos de API:
// Repositorio genérico
interface Repositorio<T, ID = string> {
buscarPorId(id: ID): Promise<T | null>;
buscarTodos(filtros?: Partial<T>): Promise<T[]>;
crear(datos: Omit<T, 'id' | 'creadoEn'>): Promise<T>;
actualizar(id: ID, datos: Partial<T>): Promise<T | null>;
eliminar(id: ID): Promise<boolean>;
}
// Implementación concreta:
interface Usuario {
id: string;
nombre: string;
email: string;
creadoEn: Date;
}
class RepositorioUsuarios implements Repositorio<Usuario> {
private usuarios: Map<string, Usuario> = new Map();
async buscarPorId(id: string): Promise<Usuario | null> {
return this.usuarios.get(id) ?? null;
}
async buscarTodos(filtros?: Partial<Usuario>): Promise<Usuario[]> {
const todos = Array.from(this.usuarios.values());
if (!filtros) return todos;
return todos.filter(u =>
Object.entries(filtros).every(([k, v]) => u[k as keyof Usuario] === v)
);
}
async crear(datos: Omit<Usuario, 'id' | 'creadoEn'>): Promise<Usuario> {
const usuario: Usuario = {
...datos,
id: crypto.randomUUID(),
creadoEn: new Date(),
};
this.usuarios.set(usuario.id, usuario);
return usuario;
}
async actualizar(id: string, datos: Partial<Usuario>): Promise<Usuario | null> {
const existente = this.usuarios.get(id);
if (!existente) return null;
const actualizado = { ...existente, ...datos };
this.usuarios.set(id, actualizado);
return actualizado;
}
async eliminar(id: string): Promise<boolean> {
return this.usuarios.delete(id);
}
}Tipos por defecto en genéricos
Los parámetros de tipo pueden tener valores por defecto, igual que los parámetros de función:
// T tiene como valor por defecto 'unknown'
interface RespuestaAPI<T = unknown> {
datos: T;
exitoso: boolean;
mensaje: string;
}
// Sin especificar: T = unknown
const respuestaGenérica: RespuestaAPI = { datos: 'algo', exitoso: true, mensaje: 'OK' };
respuestaGenérica.datos; // tipo: unknown
// Con especificación: T = Usuario
const respuestaUsuario: RespuestaAPI<Usuario> = {
datos: { id: 'u1', nombre: 'Ana', email: '[email protected]', creadoEn: new Date() },
exitoso: true,
mensaje: 'Usuario encontrado',
};
respuestaUsuario.datos.nombre; // tipo: string ✅Múltiples parámetros de tipo
Puedes tener tantos parámetros de tipo como necesites:
// Dos tipos: entrada y salida
function transformar<Entrada, Salida>(
valor: Entrada,
fn: (v: Entrada) => Salida
): Salida {
return fn(valor);
}
const resultado = transformar(42, n => `El número es ${n}`);
// Entrada = number, Salida = string
// resultado: string
// Tres tipos: clave, valor, transformado
function transformarObjeto<K extends string, V, T>(
objeto: Record<K, V>,
fn: (clave: K, valor: V) => T
): Record<K, T> {
const resultado = {} as Record<K, T>;
for (const [clave, valor] of Object.entries(objeto)) {
resultado[clave as K] = fn(clave as K, valor as V);
}
return resultado;
}
const precios = { manzana: 1.5, banana: 0.8, naranja: 2.0 };
const preciosFormateados = transformarObjeto(precios, (nombre, precio) =>
`${nombre}: $${precio.toFixed(2)}`
);
// { manzana: 'manzana: $1.50', banana: 'banana: $0.80', naranja: 'naranja: $2.00' }Clases genéricas
Las clases genéricas parametrizan el comportamiento de la clase por tipo:
class Cola<T> {
private elementos: T[] = [];
encolar(elemento: T): void {
this.elementos.push(elemento);
}
desencolar(): T | undefined {
return this.elementos.shift();
}
espiar(): T | undefined {
return this.elementos[0];
}
get tamaño(): number {
return this.elementos.length;
}
get estaVacía(): boolean {
return this.elementos.length === 0;
}
comoArreglo(): readonly T[] {
return [...this.elementos];
}
}
// Cola de tareas
interface Tarea {
id: string;
tipo: 'email' | 'sms' | 'push';
destinatario: string;
mensaje: string;
}
const colaTareas = new Cola<Tarea>();
colaTareas.encolar({ id: 't1', tipo: 'email', destinatario: '[email protected]', mensaje: 'Bienvenida' });
colaTareas.encolar({ id: 't2', tipo: 'sms', destinatario: '+591-700-00000', mensaje: 'Código: 1234' });
while (!colaTareas.estaVacía) {
const tarea = colaTareas.desencolar();
if (tarea) {
console.log(`Procesando ${tarea.tipo} para ${tarea.destinatario}`);
}
}Genéricos en type aliases
Los type aliases también pueden ser genéricos, y son especialmente poderosos para crear tipos utilitarios:
// Hacer nullable cualquier tipo
type Nullable<T> = T | null;
// Hacer todas las propiedades nullable
type NullableProps<T> = {
[K in keyof T]: T[K] | null;
};
// Función que puede retornar el valor o un error
type Resultado<T, E = Error> =
| { ok: true; valor: T }
| { ok: false; error: E };
// Función async que puede fallar con información tipada
type PromesaResultado<T, E = string> = Promise<Resultado<T, E>>;
// Uso:
async function buscarProducto(id: string): PromesaResultado<Producto, 'no_encontrado' | 'sin_acceso'> {
const producto = await baseDeDatos.productos.get(id);
if (!producto) return { ok: false, error: 'no_encontrado' };
return { ok: true, valor: producto };
}Combinando genéricos con discriminated unions
La combinación más poderosa:
type EstadoAsync<T> =
| { estado: 'idle' }
| { estado: 'cargando' }
| { estado: 'exitoso'; datos: T; cargadoEn: Date }
| { estado: 'error'; mensaje: string; código?: number };
function crearEstadoIdle<T>(): EstadoAsync<T> {
return { estado: 'idle' };
}
function crearEstadoCargando<T>(): EstadoAsync<T> {
return { estado: 'cargando' };
}
function crearEstadoExitoso<T>(datos: T): EstadoAsync<T> {
return { estado: 'exitoso', datos, cargadoEn: new Date() };
}
function crearEstadoError<T>(mensaje: string, código?: number): EstadoAsync<T> {
return { estado: 'error', mensaje, código };
}
// En un componente o servicio:
let estadoUsuario: EstadoAsync<Usuario> = crearEstadoIdle();
async function cargarUsuario(id: string): Promise<void> {
estadoUsuario = crearEstadoCargando();
try {
const usuario = await fetch(`/api/usuarios/${id}`).then(r => r.json() as Promise<Usuario>);
estadoUsuario = crearEstadoExitoso(usuario);
} catch (err) {
estadoUsuario = crearEstadoError(err instanceof Error ? err.message : 'Error desconocido');
}
}
function renderizarEstado(estado: EstadoAsync<Usuario>): string {
switch (estado.estado) {
case 'idle': return 'Sin cargar';
case 'cargando': return 'Cargando...';
case 'exitoso': return `Usuario: ${estado.datos.nombre}`;
case 'error': return `Error: ${estado.mensaje}`;
}
}Los genéricos transforman TypeScript de un sistema de tipos básico en un lenguaje de programación a nivel de tipos. En la próxima lección iremos más profundo con constraints, tipos condicionales y los tipos utilitarios avanzados que forman el vocabulario de TypeScript profesional.
Inicia sesión para guardar tu progreso