Saltar al contenido

Lección 10 de 20 · Lectura y práctica

Manejo de Estado con Tipos Discriminados

Concepto claveLos 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.

Del concepto a la práctica

Taller de esta lección

Un estado discriminado hace imposible leer datos de éxito mientras la operación está cargando o falló. El estrechamiento ocurre al revisar una propiedad literal común.

Ejemplo resuelto

ts
type State<T> = { kind: "loading" } | { kind: "success"; data: T } | { kind: "error"; message: string };
function text(state: State<string>) {
  switch (state.kind) {
    case "loading": return "cargando";
    case "success": return state.data;
    case "error": return `error: ${state.message}`;
    default: { const unreachable: never = state; return unreachable; }
  }
}
console.log(text({ kind: "success", data: "listo" })); // listo

Pruébalo

  1. Añade un estado empty sin datos.

  2. Incorpóralo al switch.

  3. Intenta leer data desde error.

  4. Comprueba el error de compilación.

Comprueba tu resultado

empty debe producir un mensaje propio y success debe producir listo.

Error frecuente: usar data?: T y message?: string juntos. Corrígelo con variantes separadas; así cada rama contiene exactamente sus propiedades y el compilador detecta casos omitidos.

El default con never introduce la comprobación exhaustiva que faltaba: al añadir empty sin una rama, TypeScript debe fallar en esa asignación. Con el switch original el retorno podía ampliarse a undefined según configuración. Prueba las salidas cargando, listo y error: fallo, después añade empty y su salida sin tareas. El estado loading no lleva data; una respuesta vieja debe descartarse al cambiar de solicitud para que los tipos no oculten una carrera de ejecución.

Loading, success, error y empty tienen propiedades diferentes; switch estrecha el tipo y never detecta casos sin cubrir.
CursaloMapa de práctica
Cada estado tiene sus propios datos

La propiedad kind discrimina las variantes y permite usar data solo en success o message solo en error. La comprobación never detecta una variante omitida al compilar. El tipo no elimina carreras entre respuestas: la aplicación debe descartar resultados obsoletos.

Ver la infografía en tamaño completo ↗
Fuentes para profundizar

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:

  1. Define interfaces o tipos para cada estado, incluyendo una propiedad discriminante con un valor literal único (ej: type: 'loading').
  2. Crea un tipo unión que combine todos los estados posibles.
  3. Usa guardias de tipo (if o switch) 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 type o status con valores únicos (ej: 'loading', 'success').
  • Olvidar el caso default en switch: Al usar switch, no incluir un caso default puede llevar a estados no manejados. Solución: Usa default con una asignación a never para 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 tanto data como error. 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

  1. Puedo definir una unión discriminada con al menos 3 estados mutuamente excluyentes para un escenario del mundo real.
  2. Sé implementar una función que use un switch o if con el discriminante para manejar todos los casos de forma exhaustiva.
  3. He refactorizado código existente que usa tipos opcionales a tipos discriminados para eliminar estados inválidos.
  4. Puedo explicar cómo los tipos discriminados mejoran la seguridad de tipos en APIs asíncronas o manejo de estado global.
  5. Identifico y corrijo errores comunes como discriminantes no únicos o falta de exhaustividad en mi código.
  6. Uso tipos discriminados en combinación con genéricos para crear componentes reutilizables (ej: un hook de React para estado).
  7. 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:

  1. Analiza el código base: Se proporciona un hook useFetch que actualmente usa un tipo con propiedades opcionales, permitiendo estados inválidos (ej: data y error simultáneamente).
  2. Define los tipos discriminados: Crea un tipo unión FetchState con tres miembros: 'idle' (sin datos), 'loading' (en progreso), 'success' (con datos de tipo T), y 'error' (con mensaje de error). Usa una propiedad status como discriminante.
  3. Refactoriza el hook: Modifica el hook para usar FetchState en lugar del tipo original. Asegúrate de que las transiciones de estado (ej: al iniciar carga, al recibir datos) actualicen el discriminante correctamente.
  4. Implementa una función de utilidad: Añade una función getFetchMessage que tome un FetchState y devuelva un mensaje descriptivo basado en el estado, usando un switch exhaustivo.
  5. 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.

Sigue con TypeScript Avanzado: Patrones Genéricos y Tipado Profundo para Arquitecturas Complejas

Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.

Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.