En esta página
Genéricos avanzados y constraints
¿Por qué necesitas genéricos avanzados?
Los genéricos básicos (Array<T>, Promise<T>) son el punto de partida, pero TypeScript ofrece un conjunto de herramientas mucho más poderoso para modelar relaciones entre tipos de forma precisa. Cuando empiezas a construir librerías, servicios de datos o formularios reutilizables, los constraints, el operador keyof y los tipos condicionales se convierten en tus mejores aliados.
En esta lección aprenderás a:
- Restringir parámetros genéricos con
extends. - Usar
keyofpara acceder de forma segura a las propiedades de un tipo. - Aprovechar el acceso indexado para extraer sub-tipos.
- Introducir lógica condicional en el sistema de tipos con tipos condicionales.
- Capturar tipos anónimos con la palabra clave
infer.
Constraints con `extends`
Un genérico sin restricciones acepta absolutamente cualquier tipo, lo que puede ser demasiado permisivo. Los constraints limitan el conjunto de tipos válidos usando la sintaxis T extends Tipo.
// Sin constraint: T puede ser string, number, boolean, objeto...
function identidad<T>(valor: T): T {
return valor;
}
// Con constraint: T debe tener al menos la propiedad `length`
function logLongitud<T extends { length: number }>(valor: T): void {
console.log(`Longitud: ${valor.length}`);
}
logLongitud("hola"); // 4
logLongitud([1, 2, 3]); // 3
logLongitud({ length: 10 }); // 10
// logLongitud(42); // ❌ number no tiene `length`El constraint { length: number } no exige que T sea exactamente ese tipo, sino que sea un subtipo estructural de él. Así, string, Array y cualquier objeto con length son válidos.
Constraints múltiples con intersección
Puedes combinar varios requisitos con &:
interface TieneId {
id: number;
}
interface TieneNombre {
nombre: string;
}
function mostrarEntidad<T extends TieneId & TieneNombre>(entidad: T): string {
return `[${entidad.id}] ${entidad.nombre}`;
}
mostrarEntidad({ id: 1, nombre: "Tarea A", completada: false }); // válidoEl operador `keyof`
keyof T produce una unión de literales de string con los nombres de todas las propiedades de T. Es la pieza fundamental para crear funciones que acceden a propiedades de forma tipada.
interface Producto {
nombre: string;
precio: number;
enStock: boolean;
}
type ClavesProducto = keyof Producto;
// equivale a: "nombre" | "precio" | "enStock"Cuando combinas keyof con genéricos, obtienes funciones completamente type-safe:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const producto: Producto = { nombre: "Laptop", precio: 1200, enStock: true };
const nombre = getProperty(producto, "nombre"); // tipo: string
const precio = getProperty(producto, "precio"); // tipo: number
const activo = getProperty(producto, "enStock"); // tipo: booleanEl tipo de retorno T[K] es el acceso indexado: el tipo de la propiedad K dentro de T. TypeScript lo resuelve en tiempo de compilación, por lo que cada llamada devuelve el tipo exacto de la propiedad solicitada.
Acceso indexado (Indexed Access Types)
Los tipos de acceso indexado permiten extraer el tipo de una propiedad sin necesidad de un genérico:
interface Pedido {
id: number;
cliente: {
nombre: string;
email: string;
};
lineas: Array<{
productoId: number;
cantidad: number;
precioUnitario: number;
}>;
}
type Cliente = Pedido["cliente"]; // { nombre: string; email: string }
type LineaPedido = Pedido["lineas"][number]; // { productoId: number; cantidad: number; precioUnitario: number }
type EmailCliente = Pedido["cliente"]["email"]; // string[number] como índice extrae el tipo de los elementos de un array. Es extremadamente útil para derivar tipos de estructuras de datos profundas sin duplicar definiciones.
Genéricos con `keyof` en validación de formularios
Un caso de uso real es validar campos de un formulario de forma genérica:
type Errores<T> = Partial<Record<keyof T, string>>;
interface FormularioRegistro {
nombre: string;
email: string;
password: string;
}
function validarCampo<T extends object>(
datos: T,
campo: keyof T,
validator: (valor: T[keyof T]) => string | null
): Errores<T> {
const error = validator(datos[campo]);
if (error) {
return { [campo]: error } as Errores<T>;
}
return {};
}
const datos: FormularioRegistro = {
nombre: "",
email: "no-es-email",
password: "123",
};
const errorNombre = validarCampo(datos, "nombre", (v) =>
typeof v === "string" && v.length === 0 ? "El nombre es obligatorio" : null
);
// { nombre: "El nombre es obligatorio" }Tipos condicionales
Los tipos condicionales añaden lógica if-else al sistema de tipos con la sintaxis T extends U ? X : Y:
type EsString<T> = T extends string ? true : false;
type A = EsString<string>; // true
type B = EsString<number>; // false
type C = EsString<"hola">; // true — los literales extienden su tipo baseTipos condicionales distributivos
Cuando T es una unión, el tipo condicional se distribuye automáticamente sobre cada miembro:
type Primitivo = string | number | boolean;
type NoBooleano<T> = T extends boolean ? never : T;
type SoloStringsYNumeros = NoBooleano<Primitivo>;
// string | numberEsto es exactamente cómo están implementados los utility types Exclude y Extract de la librería estándar.
La palabra clave `infer`
infer permite capturar un sub-tipo dentro de un tipo condicional y asignarle un nombre para usarlo en la rama true. Es la herramienta que hace posible utility types como ReturnType y Parameters.
// Implementación interna de ReturnType:
type MiReturnType<T extends (...args: unknown[]) => unknown> =
T extends (...args: unknown[]) => infer R ? R : never;
function calcularDescuento(precio: number, porcentaje: number): number {
return precio * (1 - porcentaje / 100);
}
type ResultadoDescuento = MiReturnType<typeof calcularDescuento>; // numberOtro ejemplo: extraer el tipo de los elementos de un Promise:
type Awaited<T> = T extends Promise<infer U> ? U : T;
type ValorAsync = Awaited<Promise<string[]>>; // string[]
type ValorSync = Awaited<number>; // numberY para extraer el tipo del primer parámetro de una función:
type PrimerParametro<T extends (...args: unknown[]) => unknown> =
T extends (primero: infer P, ...resto: unknown[]) => unknown ? P : never;
function saludar(nombre: string, veces: number): void {
for (let i = 0; i < veces; i++) console.log(`Hola, ${nombre}`);
}
type NombreTipo = PrimerParametro<typeof saludar>; // stringEjemplo completo: getter type-safe y validador genérico
// ──────────────────────────────────────────────────────────────
// Getter type-safe
// ──────────────────────────────────────────────────────────────
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
return { ...obj, [key]: value };
}
// ──────────────────────────────────────────────────────────────
// Validador genérico de campos
// ──────────────────────────────────────────────────────────────
type Regla<V> = (valor: V) => string | null;
type EsquemaValidacion<T> = {
[K in keyof T]?: Regla<T[K]>;
};
function validarFormulario<T extends object>(
datos: T,
esquema: EsquemaValidacion<T>
): Partial<Record<keyof T, string>> {
const errores: Partial<Record<keyof T, string>> = {};
for (const campo in esquema) {
const clave = campo as keyof T;
const regla = esquema[clave];
if (regla) {
const error = regla(datos[clave]);
if (error !== null) {
errores[clave] = error;
}
}
}
return errores;
}
// ──────────────────────────────────────────────────────────────
// Uso
// ──────────────────────────────────────────────────────────────
interface FormularioContacto {
nombre: string;
email: string;
mensaje: string;
}
const datosContacto: FormularioContacto = {
nombre: "Luis",
email: "no-es-valido",
mensaje: "",
};
const erroresContacto = validarFormulario(datosContacto, {
nombre: (v) => (v.trim().length < 2 ? "Mínimo 2 caracteres" : null),
email: (v) => (v.includes("@") ? null : "Email inválido"),
mensaje: (v) => (v.trim().length === 0 ? "El mensaje no puede estar vacío" : null),
});
console.log(erroresContacto);
// { email: "Email inválido", mensaje: "El mensaje no puede estar vacío" }Resumen
| Herramienta | Sintaxis | Uso principal |
|---|---|---|
| Constraint básico | T extends Tipo |
Limitar tipos aceptados |
keyof |
keyof T |
Unión de claves de un tipo |
| Acceso indexado | T[K] |
Tipo de una propiedad concreta |
| Tipo condicional | T extends U ? X : Y |
Lógica en el sistema de tipos |
infer |
infer R (dentro de condicional) |
Capturar sub-tipos anónimos |
Estas herramientas se combinan entre sí: los utility types de la librería estándar (ReturnType, Parameters, Awaited) están todos implementados usando exactamente estos mecanismos. En la siguiente lección verás cómo usar narrowing y type guards para que TypeScript entienda qué tipo concreto tienes en cada rama de ejecución.
Inicia sesión para guardar tu progreso