TypeScript de Cero a Experto / Genéricos
Genéricos en interfaces y tipos: estructuras que se adaptan
Los genéricos no viven solo en funciones: brillan en INTERFACES y tipos. Una
Caja<T> que guarda cualquier cosa, un Resultado<T> que envuelve el éxito de una
operación, una respuesta de API tipada por su contenido. Combina genéricos con las
discriminated unions del módulo 4 y tendrás las estructuras de datos más elegantes
de TypeScript.
El contenedor universal
Interfaces y tipos genéricos
La sintaxis es la misma <T>, ahora en la declaración del tipo:
// una interface genérica: T es el tipo de lo que contiene
interface Caja<T> {
contenido: T;
vaciar(): void;
}
const cajaTexto: Caja<string> = { contenido: "hola", vaciar() {} };
const cajaNumero: Caja<number> = { contenido: 42, vaciar() {} };
cajaTexto.contenido.toUpperCase(); // ✓ TS sabe que es string
cajaNumero.contenido.toFixed(2); // ✓ TS sabe que es number
// también con type alias, idéntico:
type Par<K, V> = { clave: K; valor: V };
const edad: Par<string, number> = { clave: "edad", valor: 28 };
// estructuras anidadas: una lista de pares
type Diccionario<V> = Par<string, V>[];
const config: Diccionario<boolean> = [
{ clave: "oscuro", valor: true },
{ clave: "notificaciones", valor: false },
];Caja<T> define la forma una vez, y Caja<string> / Caja<number> la
especializan. El T fluye a donde lo uses dentro de la interface —en contenido,
en métodos, donde sea. Es el mismo poder de las funciones genéricas, aplicado a la
FORMA de tus datos.
El patrón estrella: Resultado<T>
Combina genéricos con discriminated unions (módulo 4) y obtienes una de las
estructuras más útiles de TS: un tipo que representa "una operación que salió bien
(con datos de tipo T) o mal (con un error)":
// éxito lleva datos de tipo T; fallo lleva un mensaje. Genérico + discriminada:
type Resultado<T> =
| { ok: true; datos: T }
| { ok: false; error: string };
// una función que puede fallar, tipada honestamente:
function dividir(a: number, b: number): Resultado<number> {
if (b === 0) return { ok: false, error: "División entre cero" };
return { ok: true, datos: a / b };
}
const r = dividir(10, 2);
if (r.ok) {
console.log(r.datos.toFixed(2)); // 🔍 r.datos es number, seguro
} else {
console.log("Error:", r.error); // 🔍 aquí r.error es string
}Resultado<number> dice "esto salió bien con un número, o mal con un error". El
if (r.ok) estrecha (narrowing del módulo 4) y te da acceso a datos (tipado como
T) o a error. Es una alternativa tipada y explícita a lanzar excepciones —el
patrón que usan lenguajes como Rust y que muchos equipos de TS adoptan para errores
esperados. Genérico + discriminada = elegancia pura.
Ese Estado<T> es reutilizable para CUALQUIER dato: Estado<Gasto[]>,
Estado<Usuario>, Estado<string[]> —todos comparten las cuatro fases, y solo
cambia el tipo de datos. Es exactamente cómo tipan el estado de las peticiones
las apps de React serias. Un tipo, infinitos datos.
¿Qué ventaja tiene type Resultado<T> = { ok: true; datos: T } | { ok: false; error: string } frente a tener ResultadoNumero y ResultadoTexto separados?
Mini-reto
Diseña una estructura genérica: 1) crea type Paginado<T> = { items: T[]; pagina: number; total: number; hayMas: boolean }; 2) escribe una función primeraPagina<T> (items: T[], tam: number): Paginado<T> que devuelva la primera página; 3)
pruébala con un array de números y otro de objetos, confirmando que .items
conserva el tipo. Acabas de tipar la paginación de cualquier API, una sola vez.
Qué sigue
Genéricos en funciones e interfaces, listos. Falta el tercer hogar: las CLASES. La
próxima lección construye estructuras de datos genéricas con class —una Pila<T>,
un Store<T>— y te enseña los valores POR DEFECTO de los genéricos, para cerrar el
módulo con un reto de estructura tipada completa.