Concepto clave
Los tipos genéricos en TypeScript son plantillas de tipos que permiten crear componentes reutilizables y flexibles. Piensa en ellos como moldes para galletas: el molde (genérico) define la forma, pero puedes usar diferentes masas (tipos concretos) para crear galletas variadas. Esto es fundamental en arquitecturas complejas donde necesitas abstraer lógica sin perder seguridad de tipos.
El tipado profundo va más allá de anotaciones superficiales, utilizando tipos condicionales, mapeados y utilitarios para modelar relaciones complejas. Imagina construir un sistema de permisos donde cada usuario tiene roles específicos con accesos detallados; el tipado profundo te permite capturar estas relaciones en tiempo de compilación, previniendo errores que en JavaScript solo aparecerían en runtime.
Cómo funciona en la práctica
Veamos un ejemplo paso a paso de un generic constraint con tipos condicionales:
type ExtractProperty = T[K];
interface User {
id: number;
name: string;
email: string;
}
// Uso:
type UserName = ExtractProperty; // string
Este código define un tipo genérico que extrae una propiedad específica de un objeto. Primero, T representa cualquier tipo, y K está restringido a las claves de T. Luego, T[K] accede al tipo de esa propiedad. En el ejemplo, UserName se infiere como string porque name es una clave de User con tipo string.
Caso de estudio
En una librería de UI para arquitecturas complejas, necesitas un componente DataTable que maneje diferentes fuentes de datos. Usamos genéricos para tipar las columnas y filas dinámicamente:
interface Column {
key: keyof T;
label: string;
render?: (value: T[keyof T]) => React.ReactNode;
}
function DataTable({ data, columns }: { data: T[]; columns: Column[] }) {
return (
{columns.map(col => (
))}
{data.map((item, index) => (
{columns.map(col => (
))}
))}
{col.label}
{col.render ? col.render(item[col.key]) : String(item[col.key])}
);
}
// Uso con tipo concreto:
interface Product {
id: number;
name: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: "Laptop", price: 999 },
{ id: 2, name: "Mouse", price: 25 }
];
const columns: Column[] = [
{ key: "name", label: "Nombre" },
{ key: "price", label: "Precio", render: value => `$${value}` }
];
// DataTable({ data: products, columns });
Este componente acepta cualquier tipo T, asegurando que las columnas se alineen con las propiedades de los datos. El tipado profundo con keyof T previene errores como acceder a propiedades inexistentes.
Errores comunes
- Usar
anyen genéricos: Esto anula los beneficios de TypeScript. En lugar defunction process(data: T), define restricciones claras comoT extends object. - Ignorar tipos condicionales: No aprovechar
extendspara lógica de tipos puede llevar a código redundante. Por ejemplo, en lugar de duplicar funciones para diferentes tipos, usaT extends string ? ... : .... - Tipos demasiado amplios: En arquitecturas complejas, genéricos como
Tsin restricciones pueden causar errores sutiles. Siempre limita conextendspara reflejar el dominio real. - No usar tipos utilitarios: Reimplementar funcionalidad que ya provee TypeScript, como
PartialoPick, aumenta la complejidad innecesariamente. - Descuidar el rendimiento: Tipos recursivos o muy complejos pueden ralentizar el compilador. En grandes proyectos, optimiza con tipos intermedios o
typeen lugar deinterfacecuando sea posible.
Checklist de dominio
- Puedo crear un tipo genérico con múltiples parámetros y restricciones (
extends). - Utilizo tipos condicionales (
T extends U ? X : Y) para modelar lógica avanzada. - Aplico tipos mapeados (
{ [K in keyof T]: ... }) para transformar estructuras. - Integro genéricos en componentes de librerías sin perder inferencia de tipos.
- Evito
anyyunknownen contextos genéricos, usando uniones oneveren su lugar. - Documento genéricos complejos con comentarios que expliquen su propósito en la arquitectura.
- Pruebo tipos con herramientas como
tsdpara asegurar comportamientos esperados.
Implementa un sistema de caché tipado con genéricos
En este ejercicio, crearás un sistema de caché genérico para una arquitectura frontend compleja. Sigue estos pasos:
- Define una interfaz
Cachecon métodosget(key: string): T | null,set(key: string, value: T): void, yclear(): void. - Implementa la clase
GenericCacheque cumpla con la interfaz, usando unMapinterno para almacenar pares clave-valor. - Añade un método opcional
update(key: string, updater: (current: T) => T): voidque actualice un valor existente de forma segura. - Crea un tipo de utilidad
CacheConfigque permita configurar claves predefinidas y sus tipos asociados, por ejemplo, para un caché de usuario y producto. - Prueba tu implementación con al menos dos tipos concretos (ej.,
UseryProduct) y verifica que TypeScript infiera los tipos correctamente.
- Usa
Mappara el almacenamiento interno, ya que ofrece mejor rendimiento que objetos planos. - Para el tipo
CacheConfig, considera usar un tipo mapeado como{ [Key in K]: V }para definir las claves. - En el método
update, asegúrate de verificar si la clave existe antes de aplicar el updater para evitar errores.
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.