Español

Explora las uniones discriminadas de TypeScript, una herramienta poderosa para construir máquinas de estado robustas y con seguridad de tipos. Aprende a definir estados y transiciones.

Uniones Discrimnadas de TypeScript: Construyendo Máquinas de Estado con Seguridad de Tipos

En el ámbito del desarrollo de software, gestionar eficazmente el estado de la aplicación es crucial. Las máquinas de estado proporcionan una abstracción poderosa para modelar sistemas con estado complejos, asegurando un comportamiento predecible y simplificando el razonamiento sobre la lógica del sistema. TypeScript, con su robusto sistema de tipos, ofrece un mecanismo fantástico para construir máquinas de estado con seguridad de tipos utilizando uniones discriminadas (también conocidas como uniones etiquetadas o tipos de datos algebraicos).

¿Qué son las Uniones Discrimnadas?

Una unión discriminada es un tipo que representa un valor que puede ser uno de varios tipos diferentes. Cada uno de estos tipos, conocidos como miembros de la unión, comparte una propiedad común y distinta llamada discriminante o etiqueta. Este discriminante permite a TypeScript determinar con precisión qué miembro de la unión está actualmente activo, lo que permite una potente comprobación de tipos y autocompletado.

Piénsalo como un semáforo. Puede estar en uno de tres estados: Rojo, Amarillo o Verde. La propiedad 'color' actúa como el discriminante, indicándonos exactamente en qué estado se encuentra la luz.

¿Por qué usar uniones discriminadas para máquinas de estado?

Las uniones discriminadas aportan varios beneficios clave al construir máquinas de estado en TypeScript:

Definición de una máquina de estado con uniones discriminadas

Ilustremos cómo definir una máquina de estado utilizando uniones discriminadas con un ejemplo práctico: un sistema de procesamiento de pedidos. Un pedido puede estar en los siguientes estados: Pendiente, Procesando, Enviado y Entregado.

Paso 1: Definir los tipos de estado

Primero, definimos los tipos individuales para cada estado. Cada tipo tendrá una propiedad `type` que actúa como discriminante, junto con cualquier dato específico del estado.


interface Pending {
  type: "pendiente";
  orderId: string;
  customerName: string;
  items: string[];
}

interface Processing {
  type: "procesando";
  orderId: string;
  assignedAgent: string;
}

interface Shipped {
  type: "enviado";
  orderId: string;
  trackingNumber: string;
}

interface Delivered {
  type: "entregado";
  orderId: string;
  deliveryDate: Date;
}

Paso 2: Crear el tipo de unión discriminada

A continuación, creamos la unión discriminada combinando estos tipos individuales usando el operador `|` (unión).


type OrderState = Pending | Processing | Shipped | Delivered;

Ahora, `OrderState` representa un valor que puede ser `Pending`, `Processing`, `Shipped` o `Delivered`. La propiedad `type` dentro de cada estado actúa como discriminante, lo que permite a TypeScript diferenciarlos.

Manejo de las transiciones de estado

Ahora que hemos definido nuestra máquina de estado, necesitamos un mecanismo para realizar la transición entre los estados. Creemos una función `processOrder` que tome el estado actual y una acción como entrada y devuelva el nuevo estado.


interface Action {
  type: string;
  payload?: any;
}

function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    case "pendiente":
      if (action.type === "startProcessing") {
        return {
          type: "procesando",
          orderId: state.orderId,
          assignedAgent: action.payload.agentId,
        };
      }
      return state; // No state change

    case "procesando":
      if (action.type === "shipOrder") {
        return {
          type: "enviado",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // No state change

    case "enviado":
      if (action.type === "deliverOrder") {
        return {
          type: "entregado",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // No state change

    case "entregado":
      // Order is already delivered, no further actions
      return state;

    default:
      // This should never happen due to exhaustiveness checking
      return state; // Or throw an error
  }
}

Explicación

Aprovechando la comprobación de exhaustividad

La comprobación de exhaustividad de TypeScript es una característica potente que asegura que usted maneja todos los estados posibles en su máquina de estados. Si agrega un nuevo estado a la unión `OrderState` pero olvida actualizar la función `processOrder`, TypeScript marcará un error.

Para habilitar la comprobación de exhaustividad, puede utilizar el tipo `never`. Dentro del caso `default` de su declaración switch, asigne el estado a una variable de tipo `never`.


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (previous cases) ...

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // Or throw an error
  }
}

Si la declaración `switch` maneja todos los valores posibles de `OrderState`, la variable `_exhaustiveCheck` será de tipo `never` y el código se compilará. Sin embargo, si agrega un nuevo estado a la unión `OrderState` y olvida manejarlo en la declaración `switch`, la variable `_exhaustiveCheck` será de un tipo diferente, y TypeScript generará un error en tiempo de compilación, alertándolo de la falta de caso.

Ejemplos prácticos y aplicaciones

Las uniones discriminadas son aplicables en una amplia gama de escenarios más allá de los sistemas simples de procesamiento de pedidos:

Ejemplo: Gestión del estado de la interfaz de usuario

Consideremos un ejemplo simple de gestión del estado de un componente de la interfaz de usuario que recupera datos de una API. Podemos definir los siguientes estados:


interface Initial {
  type: "initial";
}

interface Loading {
  type: "loading";
}

interface Success {
  type: "success";
  data: T;
}

interface Error {
  type: "error";
  message: string;
}

type UIState = Initial | Loading | Success | Error;

function renderUI(state: UIState): React.ReactNode {
  switch (state.type) {
    case "initial":
      return 

Haz clic en el botón para cargar datos.

; case "loading": return

Cargando...

; case "success": return
{JSON.stringify(state.data, null, 2)}
; case "error": return

Error: {state.message}

; default: const _exhaustiveCheck: never = state; return _exhaustiveCheck; } }

Este ejemplo demuestra cómo se pueden utilizar las uniones discriminadas para gestionar eficazmente los diferentes estados de un componente de la interfaz de usuario, asegurando que la interfaz de usuario se represente correctamente en función del estado actual. La función `renderUI` maneja cada estado de forma adecuada, proporcionando una forma clara y segura para el tipo de gestionar la interfaz de usuario.

Mejores prácticas para el uso de uniones discriminadas

Para utilizar eficazmente las uniones discriminadas en sus proyectos de TypeScript, considere las siguientes mejores prácticas:

Técnicas avanzadas

Tipos condicionales

Los tipos condicionales se pueden combinar con uniones discriminadas para crear máquinas de estado aún más potentes y flexibles. Por ejemplo, puede utilizar tipos condicionales para definir diferentes tipos de retorno para una función en función del estado actual.


function getData(state: UIState): T | undefined {
  if (state.type === "success") {
    return state.data;
  }
  return undefined;
}

Esta función usa una simple declaración `if` pero podría hacerse más robusta usando tipos condicionales para asegurar que siempre se retorne un tipo específico.

Tipos de utilidad

Los tipos de utilidad de TypeScript, como `Extract` y `Omit`, pueden ser útiles cuando se trabaja con uniones discriminadas. `Extract` le permite extraer miembros específicos de un tipo de unión basándose en una condición, mientras que `Omit` le permite eliminar propiedades de un tipo.


// Extraer el estado "success" de la unión UIState
type SuccessState = Extract, { type: "success" }>;

// Omitir la propiedad 'message' de la interfaz Error
type ErrorWithoutMessage = Omit;

Ejemplos del mundo real en diferentes industrias

El poder de las uniones discriminadas se extiende a varias industrias y dominios de aplicación:

Conclusión

Las uniones discriminadas de TypeScript proporcionan una forma poderosa y segura para el tipo de construir máquinas de estado. Al definir claramente los estados y transiciones posibles, puede crear un código más robusto, mantenible y comprensible. La combinación de la seguridad de tipos, la comprobación de exhaustividad y el autocompletado mejorado convierte a las uniones discriminadas en una herramienta invaluable para cualquier desarrollador de TypeScript que se ocupe de la gestión de estados complejos. Adopte las uniones discriminadas en su próximo proyecto y experimente de primera mano los beneficios de la gestión de estados con seguridad de tipos. Como hemos demostrado con diversos ejemplos desde el comercio electrónico hasta la atención médica, y la logística a la educación, el principio de la gestión de estados con seguridad de tipos a través de uniones discriminadas es universalmente aplicable.

Ya sea que esté construyendo un componente de interfaz de usuario simple o una aplicación empresarial compleja, las uniones discriminadas pueden ayudarle a gestionar el estado de manera más eficaz y reducir el riesgo de errores en tiempo de ejecución. Así que, sumérjase y explore el mundo de las máquinas de estado con seguridad de tipos con TypeScript.