Concepto clave
Los genéricos con restricciones y parámetros por defecto son dos herramientas avanzadas en TypeScript que permiten crear tipos más seguros y flexibles para arquitecturas complejas. Los genéricos con restricciones limitan los tipos que pueden ser usados como argumentos genéricos mediante la palabra clave extends, asegurando que solo tipos con ciertas propiedades o estructuras sean aceptados. Por ejemplo, puedes restringir un genérico a solo tipos que tengan una propiedad id, lo que previene errores en tiempo de ejecución al acceder a esa propiedad.
Los parámetros por defecto en genéricos funcionan de manera similar a los parámetros por defecto en funciones: permiten definir un tipo predeterminado para un parámetro genérico si no se proporciona uno explícitamente. Esto es útil para crear APIs más amigables donde ciertos tipos pueden ser inferidos automáticamente, reduciendo la verbosidad del código. En el mundo real, piensa en una biblioteca de componentes UI: las restricciones aseguran que solo se puedan pasar props válidas, y los parámetros por defecto permiten que los componentes tengan configuraciones predefinidas sin necesidad de especificarlas cada vez.
Cómo funciona en la práctica
Veamos un ejemplo paso a paso para entender cómo aplicar estas técnicas. Supongamos que estamos construyendo un sistema de gestión de usuarios en una aplicación frontend compleja.
// Paso 1: Definir una interfaz base para restricciones
interface Identifiable {
id: string;
}
// Paso 2: Crear una función genérica con restricción
function getById(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// Paso 3: Usar parámetros por defecto en un tipo genérico
type PaginatedResponse = {
data: T[];
meta: Meta;
};
// Paso 4: Aplicar en un caso real
const users: Identifiable[] = [{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }];
const user = getById(users, "1"); // TypeScript infiere que user es de tipo Identifiable
const response: PaginatedResponse = { data: users, meta: { page: 1, total: 2 } };
// Aquí, T se infiere como any y Meta como el tipo por defectoEn este ejemplo, getById solo acepta arrays de objetos que cumplan con Identifiable, y PaginatedResponse usa tipos por defecto para simplificar su uso cuando no se especifican explícitamente.
Caso de estudio
Imagina que eres un Frontend Engineer trabajando en una librería de componentes para un dashboard empresarial. Necesitas crear un componente DataTable que pueda manejar diferentes tipos de datos con paginación y filtros. Usando genéricos con restricciones y parámetros por defecto, puedes diseñar una API robusta y flexible.
// Definir restricciones para los datos de la tabla
interface TableData {
key: string;
[key: string]: any;
}
// Componente genérico con restricciones y parámetros por defecto
type DataTableProps = {
data: T[];
columns: Array<{ key: keyof T; label: string }>;
sortBy?: SortBy;
onSort?: (key: SortBy) => void;
pagination?: {
page: number;
pageSize: number;
total: number;
};
};
// Ejemplo de uso en una aplicación real
interface UserData extends TableData {
key: string; // Heredado de TableData
name: string;
email: string;
role: string;
}
const userProps: DataTableProps = {
data: [
{ key: "1", name: "Alice", email: "[email protected]", role: "Admin" },
{ key: "2", name: "Bob", email: "[email protected]", role: "User" }
],
columns: [
{ key: "name", label: "Nombre" },
{ key: "email", label: "Email" },
{ key: "role", label: "Rol" }
],
sortBy: "name", // TypeScript infiere que sortBy debe ser una clave de UserData
pagination: { page: 1, pageSize: 10, total: 2 }
};
// Sin parámetros por defecto, tendrías que especificar SortBy explícitamente:
// type DataTableProps = ...
// Esto hace que SortBy sea opcional y se infiera automáticamenteEste caso muestra cómo estas técnicas permiten crear componentes reutilizables que se adaptan a diferentes tipos de datos mientras mantienen la seguridad de tipos.
Errores comunes
- Restricciones demasiado estrictas: Usar
extendscon tipos muy específicos puede limitar la reutilización. Por ejemplo,T extends { id: string; name: string }solo funciona para objetos con exactamente esas propiedades. Solución: Define interfaces base más genéricas y usa uniones o intersecciones de tipos si necesitas más flexibilidad. - Olvidar que los parámetros por defecto no son inferidos automáticamente en todos los casos: Si un parámetro genérico con valor por defecto se usa en una posición donde TypeScript no puede inferir, puede causar errores. Solución: Especifica tipos explícitamente cuando la inferencia falle, o ajusta el diseño para hacer los tipos más deducibles.
- Confundir restricciones con tipos literales: Algunos desarrolladores intentan usar
extendspara restringir a valores literales (e.g.,T extends "admin" | "user"), lo que puede funcionar pero limita la escalabilidad. Solución: Usa uniones de tipos literales solo cuando sea necesario, y considera enums o tipos string más amplios para casos dinámicos. - No probar con tipos complejos: En arquitecturas avanzadas, los genéricos pueden anidarse o combinarse, llevando a errores sutiles. Solución: Escribe tests de tipos usando herramientas como
tsdo verifica con ejemplos edge cases en tu IDE.
Checklist de dominio
- Puedo definir una función genérica con una restricción usando
extendspara asegurar que solo ciertos tipos sean aceptados. - Sé cómo usar parámetros por defecto en tipos genéricos para simplificar APIs y reducir código boilerplate.
- He aplicado estas técnicas en un componente o función real de mi proyecto, mejorando la seguridad de tipos.
- Puedo identificar y corregir errores comunes como restricciones demasiado estrictas o problemas de inferencia.
- Entiendo cómo combinar genéricos con restricciones y parámetros por defecto en tipos anidados para arquitecturas complejas.
- He experimentado con casos edge, como usar uniones de tipos o tipos condicionales junto con estas características.
- Puedo explicar a un colega junior la diferencia entre
T extends Somethingy un tipo por defecto, con ejemplos prácticos.
Implementa un sistema de caché genérico con restricciones y parámetros por defecto
En este ejercicio, crearás un sistema de caché genérico para una aplicación frontend que maneje diferentes tipos de datos, como usuarios, productos o configuraciones. Sigue estos pasos:
- Define una interfaz
Cacheablecon una propiedadidde tipo string y una propiedad opcionaltimestampde tipo number. - Crea una clase genérica
Cacheque tenga métodos para agregar, obtener y eliminar items del caché. Usa un Map interno para almacenar los items. - Añade un parámetro por defecto al genérico para el tipo de clave del caché, por ejemplo,
Cache, donde KeyType por defecto sea string pero pueda ser sobrescrito. - Implementa un método
getAll()que devuelva todos los items del caché como un array de tipo T. - Prueba tu implementación con al menos dos tipos diferentes que extiendan
Cacheable, comoUseryProduct, y verifica que TypeScript infiera los tipos correctamente.
Objetivo: Asegurar que el caché solo acepte tipos que cumplan con Cacheable y permita flexibilidad en la clave de almacenamiento.
- Recuerda que
extendsen genéricos restringe T a tipos que cumplan con la interfaz, no a la interfaz misma. - Para el parámetro por defecto, define KeyType en la declaración de la clase y úsalo en los métodos donde sea relevante, como en el Map.
- Prueba con tipos complejos, como un User con propiedades adicionales, para asegurar que la restricción funciona correctamente.
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.