Patrones de diseño avanzados en TypeScript para aplicaciones en producción real
Hace unos meses revisé el repositorio de un proyecto TypeScript con más de dos años en producción. Al abrir el archivo de configuración del cliente principal, encontré comentarios como // TODO: quitar este any y casteos del tipo const data = response as unknown as UserData desperdigados por todo el código.
El equipo se quejaba de que el compilador de TypeScript "les molestaba" en lugar de ayudarles.
El problema era que estaban usando TypeScript simplemente como "JavaScript con anotaciones de tipo superficiales". No estaban aprovechando el sistema de tipos algebraicos ni los patrones de diseño expresivos que convierten a TypeScript en uno de los lenguajes más potentes para construir software resiliente.
Conocer patrones de diseño avanzados en TypeScript no es para aprobar una entrevista técnica. Es lo que separa el código frágil del código mantenible que soporta años de evolución en producción.
El espejismo del casting as Type
El primer síntoma de una base de código TypeScript débil es el uso indiscriminado de aserciones de tipo (as).
Cuando escribes as UserData, le estás diciendo al compilador: "Cállate, yo sé más que tú". Le estás quitando a TypeScript su superpoder principal: garantizar en tiempo de compilación que tus estructuras de datos son válidas.
Como ya explicamos en nuestro análisis sobre programación defensiva en TypeScript, forzar tipos sin validación es la receta perfecta para lanzar excepciones TypeError: Cannot read properties of undefined en medio de la noche.
3 Patrones de Diseño Esenciales para Developers Senior
1. El Patrón Result (Discriminated Unions para manejo de errores)
En lugar de lanzar excepciones con throw (que son invisibles en la firma de tus funciones), utiliza un tipo Result explícito basado en uniones discriminadas:
type Success<T> = { readonly ok: true; readonly value: T };
type Failure<E> = { readonly ok: false; readonly error: E };
type Result<T, E = Error> = Success<T> | Failure<E>;
function parseConfig(rawJson: string): Result<AppConfig, ParseError> {
try {
const data = JSON.parse(rawJson);
if (!data.apiKey) {
return { ok: false, error: new ParseError("apiKey es requerida") };
}
return { ok: true, value: data as AppConfig };
} catch (e) {
return { ok: false, error: new ParseError("JSON inválido") };
}
}
// Uso obligatorio y seguro:
const result = parseConfig(rawString);
if (result.ok) {
console.log(result.value.apiKey); // TypeScript sabe que ok es true y infiere el tipo de 'value'
} else {
console.error(result.error.message); // TypeScript infiere el tipo de 'error'
}
2. El Patrón Strategy para Proveedores de IA y Servicios
Cuando construyes aplicaciones que interactúan con múltiples modelos de IA (OpenAI, Anthropic Claude, Ollama en local), el patrón Strategy te permite intercambiar algoritmos y proveedores sin modificar el código cliente:
interface AIProviderStrategy {
readonly name: string;
generateCompletion(prompt: string): Promise<string>;
}
class AnthropicStrategy implements AIProviderStrategy {
readonly name = "anthropic";
async generateCompletion(prompt: string): Promise<string> {
// Lógica específica para Anthropic API
return "Respuesta de Claude";
}
}
class OllamaLocalStrategy implements AIProviderStrategy {
readonly name = "ollama";
async generateCompletion(prompt: string): Promise<string> {
// Lógica específica para modelo en local
return "Respuesta de LLM local";
}
}
class AIService {
constructor(private strategy: AIProviderStrategy) {}
setStrategy(newStrategy: AIProviderStrategy) {
this.strategy = newStrategy;
}
async run(prompt: string) {
return await this.strategy.generateCompletion(prompt);
}
}
3. Builder Pattern con Validación en Tiempo de Compilación
El patrón Builder permite crear objetos complejos garantizando que todos los parámetros requeridos se hayan establecido antes de instanciar la clase:
type CompleteState = { host: string; port: number };
class DatabaseConfigBuilder<State extends Partial<CompleteState> = {}> {
private constructor(private readonly config: Partial<CompleteState>) {}
static create(): DatabaseConfigBuilder<{}> {
return new DatabaseConfigBuilder({});
}
setHost(host: string): DatabaseConfigBuilder<State & { host: string }> {
return new DatabaseConfigBuilder({ ...this.config, host });
}
setPort(port: number): DatabaseConfigBuilder<State & { port: number }> {
return new DatabaseConfigBuilder({ ...this.config, port });
}
build(this: DatabaseConfigBuilder<CompleteState>): DatabaseConfig {
return new DatabaseConfig(this.config.host, this.config.port);
}
}
Si intentas llamar a .build() sin haber establecido setHost() y setPort(), TypeScript rechazará la compilación de forma inmediata.
Diseñar aplicaciones en TypeScript utilizando patrones expresivos y tipos algebraicos previene el 90% de los errores en producción antes de que el código toque el servidor.
Al igual que discutimos al construir agentes de voz en tiempo real con TypeScript, el rigor en el tipado y en los contratos es lo que permite que tu código escale sin romperse. Y si combinas estos patrones con técnicas de graph engineering, tus aplicaciones serán limpias, modulares e inexpugnables.
Si deseas dominar TypeScript avanzado y patrones de arquitectura modernos, explora los Cursos de Dominicode. Y si quieres construir software de producción en comunidad con otros desarrolladores senior, te esperamos en Dominicode Labs.
Preguntas frecuentes
¿Cuándo debo preferir type sobre interface en TypeScript?
Como regla general: usa interface cuando estés definiendo contratos de objetos que pueden ser extendidos o implementados por clases (OOP). Usa type para uniones, tuplas, tipos primitivos mapeados y combinaciones algebraicas de tipos.
¿El uso de tipos avanzados degrada el rendimiento de la aplicación en producción?
No. Todo el sistema de tipos de TypeScript se elimina por completo durante el proceso de transpilación a JavaScript. Tu bundle de producción solo contiene JavaScript puro, por lo que los tipos no agregan ni un solo byte de sobrecarga en tiempo de ejecución.
¿El patrón Result reemplaza por completo los bloques try/catch?
Se recomienda usar el patrón Result para errores de dominio previsibles (fallos de validación, usuario no encontrado, saldo insuficiente). Los bloques try/catch se reservan para excepciones no controladas a nivel de infraestructura (caída de red, falta de memoria).
¿Por qué evitar el tipo any si a veces acelera el desarrollo?
Usar any desactiva completamente el verificador de tipos de TypeScript para esa variable y para todas las expresiones derivadas de ella. Si necesitas un tipo genérico desconocido temporalmente, utiliza siempre unknown y realiza narrowing con guardas de tipo (type guards).
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
