Manejo de Estado con Tipos Discriminados
Concepto clave
Los tipos discriminados (también conocidos como tagged unions o discriminated unions) son un patrón en TypeScript que permite modelar estados mutuamente excluyentes con precisión de tipos. Imagina un sistema de gestión de pedidos donde un pedido puede estar en estado "pendiente", "en proceso" o "completado". Cada estado tiene datos específicos: "pendiente" tiene fecha de creación, "en proceso" tiene ID del responsable, y "completado" tiene fecha de finalización. Sin tipos discriminados, tendrías que usar tipos opcionales que pueden llevar a errores en tiempo de ejecución.
La analogía del mundo real es una máquina expendedora: solo puedes seleccionar un producto a la vez (café, refresco o snack), y cada opción tiene un precio y un mecanismo de dispensación diferente. El tipo discriminado asegura que, al seleccionar "café", solo puedas acceder a propiedades relevantes como "temperatura" o "tamaño", no a "sabor" que pertenece a "refresco". Esto elimina estados inválidos en tiempo de compilación, reduciendo bugs en arquitecturas complejas.
Cómo funciona en la práctica
Para implementar tipos discriminados, define una unión de tipos donde cada miembro tenga una propiedad literal común (el discriminante) que TypeScript pueda usar para inferir el tipo exacto. Sigue estos pasos:
- Define interfaces o tipos para cada estado, incluyendo una propiedad discriminante con un valor literal único (ej:
type: 'loading'). - Crea un tipo unión que combine todos los estados posibles.
- Usa guardias de tipo (
ifoswitch) basados en el discriminante para acceder a propiedades específicas de cada estado de forma segura.
Ejemplo paso a paso para un sistema de autenticación:
// 1. Definir tipos para cada estado
type AuthState =
| { type: 'idle'; message: string }
| { type: 'loading'; progress: number }
| { type: 'authenticated'; user: { id: string; name: string } }
| { type: 'error'; code: number; details: string };
// 2. Función que maneja el estado de forma segura
function handleAuthState(state: AuthState): string {
switch (state.type) {
case 'idle':
return `Listo: ${state.message}`; // Acceso seguro a 'message'
case 'loading':
return `Cargando: ${state.progress}%`; // Acceso seguro a 'progress'
case 'authenticated':
return `Usuario: ${state.user.name}`; // Acceso seguro a 'user'
case 'error':
return `Error ${state.code}: ${state.details}`; // Acceso seguro a 'code' y 'details'
default:
const exhaustiveCheck: never = state; // Garantiza que todos los casos están cubiertos
throw new Error('Estado no manejado');
}
}
// 3. Uso en código
const currentState: AuthState = { type: 'authenticated', user: { id: '1', name: 'Ana' } };
console.log(handleAuthState(currentState)); // Output: "Usuario: Ana"Este enfoque garantiza que, al cambiar el type, TypeScript restringe automáticamente las propiedades disponibles, previniendo accesos incorrectos como state.user cuando type es "idle".
Caso de estudio
En una aplicación de e-commerce con arquitectura compleja, modelamos el estado de un carrito de compras usando tipos discriminados para manejar carga, éxito y errores. Supongamos que el carrito puede estar en tres estados: vacío (sin items), activo (con items y total), o error (con mensaje de fallo). Implementamos esto para una API que gestiona actualizaciones en tiempo real.
type CartItem = { id: string; name: string; price: number; quantity: number };
type CartState =
| { status: 'empty'; timestamp: Date }
| { status: 'active'; items: CartItem[]; total: number; discountCode?: string }
| { status: 'error'; message: string; retryable: boolean };
// Función para calcular impuestos solo en estado 'active'
function calculateTax(state: CartState): number {
if (state.status === 'active') {
return state.total * 0.16; // TypeScript sabe que 'total' existe aquí
}
return 0; // Para otros estados, no hay impuestos
}
// Ejemplo de uso en un flujo de API
async function fetchCart(): Promise {
try {
const response = await fetch('/api/cart');
const data = await response.json();
if (data.items && data.items.length > 0) {
return {
status: 'active',
items: data.items,
total: data.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
discountCode: data.discountCode
};
} else {
return { status: 'empty', timestamp: new Date() };
}
} catch (err) {
return { status: 'error', message: 'Fallo en la red', retryable: true };
}
}
// Uso en componente de UI
const cartState = await fetchCart();
switch (cartState.status) {
case 'empty':
console.log(`Carrito vacío desde ${cartState.timestamp}`);
break;
case 'active':
console.log(`Items: ${cartState.items.length}, Total: $${cartState.total}`);
if (cartState.discountCode) {
console.log(`Código de descuento: ${cartState.discountCode}`);
}
break;
case 'error':
console.error(`Error: ${cartState.message}, Reintentable: ${cartState.retryable}`);
break;
} Este caso muestra cómo los tipos discriminados permiten un manejo de estado robusto en APIs, evitando accesos a items cuando el carrito está vacío o en error, lo que es crítico para arquitecturas escalables.
Errores comunes
- No usar un discriminante único: Si los tipos en la unión no tienen una propiedad literal común o tienen valores solapados, TypeScript no puede discriminar correctamente. Solución: Asegúrate de que cada tipo tenga una propiedad como
typeostatuscon valores únicos (ej:'loading','success'). - Olvidar el caso default en switch: Al usar
switch, no incluir un casodefaultpuede llevar a estados no manejados. Solución: Usadefaultcon una asignación aneverpara garantizar exhaustividad, como se muestra en el ejemplo práctico. - Usar tipos opcionales en lugar de uniones: Modelar estados con propiedades opcionales (ej:
{ data?: T; error?: string }) permite estados inválidos como tener tantodatacomoerror. Solución: Refactoriza a tipos discriminados para hacer estados mutuamente excluyentes. - No aprovechar la inferencia de TypeScript: Al escribir guardias de tipo manuales innecesarias en lugar de confiar en el discriminante. Solución: Deja que TypeScript infiera automáticamente basado en la propiedad discriminante; evita casts explícitos como
as. - Ignorar el rendimiento en uniones grandes: En uniones con muchos miembros (más de 10), la inferencia puede ralentizar el compilador. Solución: Agrupa estados relacionados o usa técnicas avanzadas como branded types para casos extremos.
Checklist de dominio
- Puedo definir una unión discriminada con al menos 3 estados mutuamente excluyentes para un escenario del mundo real.
- Sé implementar una función que use un
switchoifcon el discriminante para manejar todos los casos de forma exhaustiva. - He refactorizado código existente que usa tipos opcionales a tipos discriminados para eliminar estados inválidos.
- Puedo explicar cómo los tipos discriminados mejoran la seguridad de tipos en APIs asíncronas o manejo de estado global.
- Identifico y corrijo errores comunes como discriminantes no únicos o falta de exhaustividad en mi código.
- Uso tipos discriminados en combinación con genéricos para crear componentes reutilizables (ej: un hook de React para estado).
- Documento los tipos discriminados en mi equipo para asegurar consistencia en arquitecturas complejas.
Refactorizar un hook de React con tipos discriminados
En este ejercicio, refactorizarás un hook de React que maneja el estado de una solicitud de datos usando tipos opcionales a tipos discriminados para mejorar la seguridad de tipos. Sigue estos pasos:
- Analiza el código base: Se proporciona un hook
useFetchque actualmente usa un tipo con propiedades opcionales, permitiendo estados inválidos (ej:datayerrorsimultáneamente). - Define los tipos discriminados: Crea un tipo unión
FetchStatecon tres miembros:'idle'(sin datos),'loading'(en progreso),'success'(con datos de tipoT), y'error'(con mensaje de error). Usa una propiedadstatuscomo discriminante. - Refactoriza el hook: Modifica el hook para usar
FetchStateen lugar del tipo original. Asegúrate de que las transiciones de estado (ej: al iniciar carga, al recibir datos) actualicen el discriminante correctamente. - Implementa una función de utilidad: Añade una función
getFetchMessageque tome unFetchStatey devuelva un mensaje descriptivo basado en el estado, usando unswitchexhaustivo. - Prueba el hook: Escribe un ejemplo de uso en un componente que muestre diferentes UI para cada estado (ej: spinner para loading, datos para success, error para error).
Código base inicial (para referencia):
type FetchResult = {
data?: T;
error?: string;
loading: boolean;
};
function useFetch(url: string): FetchResult {
// Implementación simplificada
return { loading: false, data: undefined, error: undefined };
} Pistas
- Recuerda que en tipos discriminados, cada estado debe tener propiedades específicas; por ejemplo, 'success' debe incluir 'data' pero no 'error'.
- Usa un switch con caso default que asigne a 'never' para garantizar que todos los estados están manejados en getFetchMessage.
- Considera usar genéricos en FetchState
para hacer el hook reutilizable para cualquier tipo de datos.
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.