CodeForge

TypeScript de Cero a Experto / Genéricos

Constraints: genéricos con condiciones

Teoría22 min20 XP

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:

constraint.ts
// ❌ 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.ts
// 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 usuario

Esto 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.