Concepto clave
Los módulos con genéricos en TypeScript son una técnica avanzada que permite crear componentes reutilizables y tipados de forma segura en arquitecturas complejas. Imagina que estás construyendo una biblioteca de componentes frontend donde cada componente debe manejar diferentes tipos de datos (como usuarios, productos o configuraciones), pero manteniendo una interfaz consistente. Los genéricos actúan como "placeholders" de tipos que se concretan en tiempo de uso, similar a cómo una plantilla de documento se adapta a diferentes contenidos sin cambiar su estructura.
Este enfoque combina la modularidad de los módulos ES6 con el poder del tipado estático de TypeScript. En lugar de crear múltiples versiones de un módulo para cada tipo de dato, defines una sola implementación parametrizada con tipos genéricos. Esto reduce la duplicación de código y mejora el mantenimiento, especialmente en proyectos grandes donde la consistencia de tipos es crítica para evitar errores en tiempo de ejecución.
Cómo funciona en la práctica
Vamos a implementar un módulo genérico paso a paso. Supongamos que necesitamos un store de estado reutilizable para diferentes entidades en una aplicación frontend.
// store.ts - Módulo genérico exportado
export interface Store {
getState(): T;
setState(newState: T): void;
subscribe(listener: (state: T) => void): () => void;
}
export function createStore(initialState: T): Store {
let state: T = initialState;
const listeners: Array<(state: T) => void> = [];
return {
getState: () => state,
setState: (newState: T) => {
state = newState;
listeners.forEach(listener => listener(state));
},
subscribe: (listener) => {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
if (index > -1) listeners.splice(index, 1);
};
}
};
}Este módulo exporta una interfaz Store<T> y una función createStore<T> que puede ser instanciada con cualquier tipo T. Al usarlo, TypeScript inferirá y validará los tipos automáticamente:
// Uso en otro archivo
import { createStore } from './store';
interface UserState {
name: string;
age: number;
}
const userStore = createStore({ name: 'Ana', age: 30 });
userStore.setState({ name: 'Carlos', age: 25 }); // Válido
// userStore.setState({ name: 'Luis' }); // Error: falta 'age'Caso de estudio
En una arquitectura frontend para un e-commerce, implementamos un módulo genérico ApiClient que maneja peticiones HTTP para diferentes recursos. Este módulo usa genéricos para tipar las respuestas y parámetros, asegurando consistencia en toda la aplicación.
// apiClient.ts
export class ApiClient {
constructor(private baseUrl: string) {}
async getAll(): Promise {
const response = await fetch(`${this.baseUrl}`);
return response.json();
}
async create(data: TCreate): Promise {
const response = await fetch(this.baseUrl, {
method: 'POST',
body: JSON.stringify(data)
});
return response.json();
}
async update(id: string, data: TUpdate): Promise {
const response = await fetch(`${this.baseUrl}/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
});
return response.json();
}
}
// Uso para productos
interface Product {
id: string;
name: string;
price: number;
}
interface CreateProductDto {
name: string;
price: number;
}
interface UpdateProductDto {
price?: number;
}
const productClient = new ApiClient('/api/products');
// productClient.create({ name: 'Laptop' }); // Error: falta 'price'
// productClient.update('123', { price: 999 }); // VálidoLos genéricos con múltiples parámetros de tipo, como enApiClient<TResource, TCreate, TUpdate>, permiten modelar operaciones complejas con precisión, evitando el uso deanyy mejorando la autocompletación en IDEs.
Errores comunes
- Usar
anyen lugar de genéricos: Esto elimina los beneficios del tipado estático. En lugar defunction getItem(key: string): any, usafunction getItem<T>(key: string): Tpara preservar la seguridad de tipos. - No restringir genéricos cuando es necesario: Si un genérico debe cumplir ciertas condiciones, usa
extends. Por ejemplo,T extends { id: string }asegura queTtenga una propiedadid. - Ignorar la inferencia de tipos: TypeScript puede inferir genéricos en muchos casos. No siempre necesitas especificarlos explícitamente, lo que puede simplificar el código.
- Crear genéricos demasiado complejos: Evita anidar múltiples genéricos sin necesidad, ya que puede hacer el código difícil de leer. Prioriza la simplicidad.
- No documentar los genéricos: En módulos compartidos, documenta el propósito de cada parámetro genérico con comentarios JSDoc para mejorar la mantenibilidad.
Checklist de dominio
- Puedo implementar un módulo que exporte una clase o función con al menos un parámetro genérico.
- Sé usar
extendspara aplicar restricciones a genéricos, comoT extends Serializable. - He aplicado genéricos en un contexto real, como un store de estado o un cliente API.
- Puedo explicar la diferencia entre
Array<T>yT[](son equivalentes, pero el primero es más explícito con genéricos). - Evito el uso de
anyen módulos genéricos, optando por tipos específicos o unknown con type guards. - Comprendo cómo TypeScript infiere tipos genéricos a partir del uso y cuándo es necesario especificarlos manualmente.
- He revisado y refactorizado código existente para introducir genéricos donde haya duplicación de tipos.
Implementar un módulo genérico para formularios dinámicos
En este ejercicio, crearás un módulo TypeScript genérico que maneje formularios dinámicos en una aplicación frontend. Sigue estos pasos:
- Crea un archivo
formManager.tsque exporte una claseFormManager<T>. - Define la clase con estos métodos:
constructor(initialData: T): inicializa el estado del formulario.getField<K extends keyof T>(field: K): T[K]: devuelve el valor de un campo específico.setField<K extends keyof T>(field: K, value: T[K]): void: actualiza el valor de un campo.validate(): boolean: valida que todos los campos cumplan reglas básicas (ej., no estar vacíos si son strings).
- Usa tipos genéricos para asegurar que
getFieldysetFieldsean type-safe, evitando asignaciones incorrectas. - Implementa un ejemplo de uso en otro archivo, definiendo una interfaz
UserFormcon campos comoname: string,email: string, yage: number. - Prueba el módulo creando una instancia de
FormManager<UserForm>y llamando a sus métodos.
- Usa
keyof Tpara restringir los parámetros de campo a las claves de T. - Considera agregar un método
getData(): Tpara obtener todos los datos del formulario. - Para validación, puedes asumir que los campos string no deben estar vacíos y los números deben ser positivos.
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.