Concepto clave
Los patrones de diseño en TypeScript son soluciones reutilizables a problemas comunes en el desarrollo de software, pero con un enfoque especial en el sistema de tipos. A nivel avanzado, no se trata solo de implementar patrones clásicos como Singleton o Factory, sino de aprovechar el tipado genérico para hacerlos más seguros, flexibles y adaptables a arquitecturas complejas.
Imagina que estás construyendo una biblioteca de componentes frontend. Sin patrones de diseño, cada componente podría tener su propia lógica ad-hoc, lo que lleva a código duplicado y difícil de mantener. Con patrones bien aplicados y tipados, puedes crear una arquitectura coherente donde los tipos guían el uso correcto y previenen errores en tiempo de compilación. Por ejemplo, el patrón Strategy con genéricos permite definir algoritmos intercambiables con tipos específicos para cada contexto, asegurando que solo se usen las estrategias válidas.
Cómo funciona en la práctica
Vamos a implementar el patrón Observer con TypeScript avanzado, usando genéricos para tipar los eventos y sus datos. Este patrón es clave en arquitecturas reactivas, como en sistemas de estado o notificaciones.
// Definición de tipos genéricos para Observer
interface EventData {
type: string;
payload: any;
}
class Observable {
private observers: Array<(event: T) => void> = [];
subscribe(observer: (event: T) => void): void {
this.observers.push(observer);
}
notify(event: T): void {
this.observers.forEach(observer => observer(event));
}
}
// Uso con tipos concretos
type UserEvent = { type: 'login'; payload: { userId: string } } | { type: 'logout'; payload: null };
const userObservable = new Observable();
userObservable.subscribe(event => {
if (event.type === 'login') {
console.log(`Usuario ${event.payload.userId} ha iniciado sesión`);
}
});
// TypeScript previene errores: userObservable.notify({ type: 'error', payload: 'msg' }); // Error de tipoEn este ejemplo, el genérico T restringe los eventos a tipos específicos, evitando notificaciones con datos incorrectos. Esto es crucial en libs donde los usuarios deben seguir contratos de tipos bien definidos.
Caso de estudio
Supongamos que eres Frontend Engineer en una empresa que desarrolla una app de e-commerce con una arquitectura compleja basada en micro-frontends. Necesitas un sistema de gestión de estado que sea tipo-seguro y escalable. Implementa el patrón State con TypeScript para manejar el estado del carrito de compras.
// Estados posibles del carrito
type CartState = 'empty' | 'adding' | 'full' | 'checkout';
class CartContext {
private state: T;
constructor(initialState: T) {
this.state = initialState;
}
transition(newState: U): CartContext {
// Lógica de transición con validación de tipos
if (this.state === 'empty' && newState === 'adding') {
return new CartContext(newState);
}
// Otras transiciones...
throw new Error('Transición inválida');
}
getState(): T {
return this.state;
}
}
// Uso en la app
const cart = new CartContext('empty');
const updatedCart = cart.transition('adding'); // Válido
// const invalidCart = cart.transition('checkout'); // Error en tiempo de compilación si no es válidoEste enfoque asegura que las transiciones de estado sean tipadas y predecibles, reduciendo bugs en flujos complejos. En una lib, podrías exportar estos tipos para que otros desarrolladores los usen correctamente.
Errores comunes
- Usar any en genéricos: Evita
class Example, ya que pierdes los beneficios del tipado. En su lugar, define restricciones claras, comoT extends EventData. - No tipar retornos de métodos en patrones: En el patrón Factory, asegúrate de que los métodos devuelvan tipos concretos, no
any, para que TypeScript pueda inferir y validar usos. - Ignorar la inferencia de tipos: No sobre-tipes cuando TypeScript puede inferir; por ejemplo, en
new Observable(), el genérico se infiere automáticamente. - Implementar patrones sin considerar arquitectura: No apliques patrones por moda; evalúa si resuelven un problema real en tu contexto, como la escalabilidad en micro-frontends.
- Olvidar documentar contratos de tipos: En libs, documenta los tipos esperados para patrones, usando comentarios JSDoc o archivos .d.ts, para facilitar el uso por otros equipos.
Checklist de dominio
- ¿Puedes implementar al menos 3 patrones de diseño (ej., Observer, Factory, Strategy) con genéricos en TypeScript?
- ¿Sabes usar conditional types o mapped types para hacer patrones más flexibles, como en Builder o Proxy?
- ¿Eres capaz de detectar y corregir errores de tipo en implementaciones de patrones existentes?
- ¿Puedes diseñar una pequeña lib que exporte patrones tipados para reuso en proyectos?
- ¿Entiendes cómo los patrones con TypeScript mejoran la mantenibilidad en arquitecturas complejas, como micro-frontends?
- ¿Has aplicado patrones en proyectos reales, midiendo su impacto en reducción de bugs o mejora de performance?
- ¿Puedes explicar a un compañero la diferencia entre un patrón clásico y su versión con tipado avanzado en TypeScript?
Implementa un Patrón Factory Tipado para una Librería de Componentes UI
En este ejercicio, crearás un patrón Factory con TypeScript avanzado para generar componentes de UI en una librería frontend. Sigue estos pasos:
- Define una interfaz genérica
UIComponentcon un métodorender()que devuelva un string (simulando HTML). - Crea clases concretas como
ButtonyInputque implementenUIComponent, con propiedades específicas tipadas (ej.,label: stringpara Button). - Implementa una clase
ComponentFactorycon un método genéricocreateComponent<T extends UIComponent>(type: string, props: ...): Tque instancie el componente correcto basado en el tipo. - Asegúrate de que TypeScript infiera los tipos de las propiedades y prevenga errores, como pasar un
labela un Input. - Prueba la factory creando un Button y un Input, y llama a sus métodos
render()para ver el output.
Objetivo: Practicar el uso de genéricos y tipos para hacer un patrón Factory seguro y reutilizable en arquitecturas de libs.
Pistas- Usa un tipo union para los tipos de componente permitidos, como 'button' | 'input'.
- Considera usar conditional types en las props para mapear el tipo de componente a sus propiedades específicas.
- En la factory, usa una estructura de datos como un mapa para asociar tipos con clases constructoras.
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.