TypeScript de Cero a Experto / Genéricos
Constraints: genéricos con condiciones
Un genérico que acepta CUALQUIER tipo a veces es demasiado libre: si tu función
usa valor.length, no puede aceptar un number (que no tiene length). Las
constraints (T extends ...) ponen condiciones: "acepto cualquier tipo, siempre
que cumpla ESTO". Es el paso que convierte los genéricos de curiosidad en
herramienta seria.
El portero con una regla
extends: exigir una forma
Sin constraint, TS no te deja tocar propiedades de T (podría ser cualquier cosa,
incluso un number sin esa propiedad). Con T extends {...}, le prometes una
forma mínima:
// ❌ sin constraint: TS no sabe si T tiene .length
function largoMalo<T>(x: T): number {
return x.length;
// ❌ Property 'length' does not exist on type 'T'.
}
// ✅ con constraint: T DEBE tener una propiedad length numérica
function largo<T extends { length: number }>(x: T): number {
return x.length; // ✓ seguro: la constraint lo garantiza
}
largo("hola"); // ✓ string tiene length → 4
largo([1, 2, 3]); // ✓ array tiene length → 3
largo({ length: 10 }); // ✓ cumple la forma
largo(42);
// ❌ Argument of type 'number' is not assignable to parameter of type '{ length: number }'.T extends { length: number } se lee "T, siempre que tenga length". Dentro de la
función, TS ya te deja usar x.length con seguridad, porque la puerta garantizó
que todo lo que entra lo tiene. Y sigue siendo genérico: acepta strings, arrays,
cualquier objeto con length —solo excluye lo que no cumple. Libertad con la
condición justa.
keyof: constrainir a las claves de un objeto
La constraint más poderosa y usada: exigir que un tipo sea una CLAVE de otro. El
operador keyof da la unión de las claves de un tipo, y con él tipas el acceso a
propiedades con total seguridad:
// keyof T = unión de las claves de T. Ej: keyof { a: 1; b: 2 } = "a" | "b"
// una función que lee una propiedad, con el tipo de retorno EXACTO:
function propiedad<T, K extends keyof T>(obj: T, clave: K): T[K] {
return obj[clave];
}
const usuario = { id: 1, nombre: "Sara", activo: true };
const id = propiedad(usuario, "id"); // TS: number (T["id"])
const nombre = propiedad(usuario, "nombre"); // TS: string (T["nombre"])
const activo = propiedad(usuario, "activo"); // TS: boolean
propiedad(usuario, "email");
// ❌ Argument of type '"email"' is not assignable to parameter of type
// 'keyof typeof usuario' — 'email' no es una clave de usuarioEsto es de una precisión asombrosa. K extends keyof T garantiza que clave es
una clave REAL de obj, así que propiedad(usuario, "email") —clave inexistente—
se marca en rojo. Y el retorno T[K] (indexed access, módulo 7) es el tipo EXACTO
de esa propiedad: id devuelve number, nombre devuelve string. Una función,
tipos perfectos por clave. Este patrón es el corazón de mil utilidades tipadas.
Ese seleccionar es, casi literalmente, el utility type Pick que verás en el
módulo 6 —solo que ahora entiendes su maquinaria: K extends keyof T y el acceso
T[K]. Cuando uses Pick<Gasto, "nombre" | "valor">, sabrás exactamente qué hace
por dentro.
Quieres una función leer(obj, clave) que devuelva el valor de esa propiedad con su tipo EXACTO, y que rechace claves inexistentes. ¿Cómo la tipas?
Mini-reto
Trabaja con constraints: 1) escribe function primerCampo<T extends object>(obj: T): keyof T que devuelva la primera clave de un objeto (Object.keys(obj)[0] as keyof T); 2) escribe function tieneClave<T, K extends keyof T>(obj: T, clave: K): boolean que confirme si la propiedad existe y no es undefined; 3) prueba que
tieneClave(usuario, "email") es rechazado por TS si email no es clave. Sientes
cómo keyof ata las claves al objeto real.
Qué sigue
Has hecho FUNCIONES genéricas. Pero los genéricos también viven en INTERFACES y
tipos: una Caja<T>, un Resultado<T>, una colección tipada. La próxima lección
lleva los genéricos a las estructuras de datos, donde brillan aún más.