Português

Explore as uniões discriminadas em TypeScript, uma ferramenta poderosa para construir máquinas de estado robustas e com segurança de tipo. Aprenda a definir estados e transições.

Uniões Discriminadas em TypeScript: Construindo Máquinas de Estado com Segurança de Tipo

No domínio do desenvolvimento de software, gerenciar o estado da aplicação de forma eficaz é crucial. As máquinas de estado fornecem uma abstração poderosa para modelar sistemas complexos com estado, garantindo um comportamento previsível e simplificando o raciocínio sobre a lógica do sistema. O TypeScript, com seu sistema de tipos robusto, oferece um mecanismo fantástico para construir máquinas de estado com segurança de tipo usando uniões discriminadas (também conhecidas como uniões etiquetadas ou tipos de dados algébricos).

O que são Uniões Discriminadas?

Uma união discriminada é um tipo que representa um valor que pode ser um de vários tipos diferentes. Cada um desses tipos, conhecidos como membros da união, compartilha uma propriedade comum e distinta chamada discriminante ou tag. Esse discriminante permite que o TypeScript determine precisamente qual membro da união está atualmente ativo, permitindo uma poderosa verificação de tipo e preenchimento automático.

Pense nisso como um semáforo. Ele pode estar em um de três estados: Vermelho, Amarelo ou Verde. A propriedade 'cor' atua como o discriminante, informando exatamente em qual estado a luz está.

Por que Usar Uniões Discriminadas para Máquinas de Estado?

As uniões discriminadas trazem vários benefícios importantes ao construir máquinas de estado em TypeScript:

Definindo uma Máquina de Estado com Uniões Discriminadas

Vamos ilustrar como definir uma máquina de estado usando uniões discriminadas com um exemplo prático: um sistema de processamento de pedidos. Um pedido pode estar nos seguintes estados: Pendente, Em Processamento, Enviado e Entregue.

Passo 1: Defina os Tipos de Estado

Primeiro, definimos os tipos individuais para cada estado. Cada tipo terá uma propriedade `type` atuando como o discriminante, juntamente com quaisquer dados específicos do estado.


interface Pendente {
  type: "pendente";
  orderId: string;
  customerName: string;
  items: string[];
}

interface EmProcessamento {
  type: "emProcessamento";
  orderId: string;
  assignedAgent: string;
}

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

interface Entregue {
  type: "entregue";
  orderId: string;
  deliveryDate: Date;
}

Passo 2: Crie o Tipo de União Discriminada

Em seguida, criamos a união discriminada combinando esses tipos individuais usando o operador `|` (união).


type OrderState = Pendente | EmProcessamento | Enviado | Entregue;

Agora, `OrderState` representa um valor que pode ser `Pendente`, `EmProcessamento`, `Enviado` ou `Entregue`. A propriedade `type` dentro de cada estado atua como o discriminante, permitindo que o TypeScript os diferencie.

Manipulando Transições de Estado

Agora que definimos nossa máquina de estado, precisamos de um mecanismo para fazer a transição entre os estados. Vamos criar uma função `processOrder` que recebe o estado atual e uma ação como entrada e retorna o novo estado.


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

function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    case "pendente":
      if (action.type === "startProcessing") {
        return {
          type: "emProcessamento",
          orderId: state.orderId,
          assignedAgent: action.payload.agentId,
        };
      }
      return state; // Nenhuma alteração de estado

    case "emProcessamento":
      if (action.type === "shipOrder") {
        return {
          type: "enviado",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // Nenhuma alteração de estado

    case "enviado":
      if (action.type === "deliverOrder") {
        return {
          type: "entregue",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // Nenhuma alteração de estado

    case "entregue":
      // O pedido já foi entregue, nenhuma ação adicional
      return state;

    default:
      // Isso nunca deve acontecer devido à verificação de exaustividade
      return state; // Ou lance um erro
  }
}

Explicação

Aproveitando a Verificação de Exaustividade

A verificação de exaustividade do TypeScript é um recurso poderoso que garante que você lide com todos os estados possíveis em sua máquina de estado. Se você adicionar um novo estado à união `OrderState`, mas se esquecer de atualizar a função `processOrder`, o TypeScript sinalizará um erro.

Para habilitar a verificação de exaustividade, você pode usar o tipo `never`. Dentro do caso `default` de sua instrução switch, atribua o estado a uma variável do tipo `never`.


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (casos anteriores) ...

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // Ou lance um erro
  }
}

Se a instrução `switch` lidar com todos os valores `OrderState` possíveis, a variável `_exhaustiveCheck` será do tipo `never` e o código será compilado. No entanto, se você adicionar um novo estado à união `OrderState` e se esquecer de tratá-lo na instrução `switch`, a variável `_exhaustiveCheck` será de um tipo diferente, e o TypeScript lançará um erro de tempo de compilação, alertando você para o caso ausente.

Exemplos Práticos e Aplicações

As uniões discriminadas são aplicáveis em uma ampla gama de cenários além de sistemas simples de processamento de pedidos:

Exemplo: Gerenciamento de Estado da UI

Vamos considerar um exemplo simples de gerenciamento do estado de um componente da UI que busca dados de uma API. Podemos definir os seguintes estados:


interface Inicial {
  type: "inicial";
}

interface Carregando {
  type: "carregando";
}

interface Sucesso {
  type: "sucesso";
  data: T;
}

interface Erro {
  type: "erro";
  message: string;
}

type UIState = Inicial | Carregando | Sucesso | Erro;

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

Clique no botão para carregar os dados.

; case "carregando": return

Carregando...

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

Erro: {state.message}

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

Este exemplo demonstra como as uniões discriminadas podem ser usadas para gerenciar efetivamente os diferentes estados de um componente da UI, garantindo que a UI seja renderizada corretamente com base no estado atual. A função `renderUI` lida com cada estado adequadamente, fornecendo uma maneira clara e com segurança de tipo de gerenciar a UI.

Melhores Práticas para Usar Uniões Discriminadas

Para utilizar efetivamente as uniões discriminadas em seus projetos TypeScript, considere as seguintes melhores práticas:

Técnicas Avançadas

Tipos Condicionais

Tipos condicionais podem ser combinados com uniões discriminadas para criar máquinas de estado ainda mais poderosas e flexíveis. Por exemplo, você pode usar tipos condicionais para definir diferentes tipos de retorno para uma função com base no estado atual.


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

Esta função usa uma simples instrução `if`, mas poderia ser tornada mais robusta usando tipos condicionais para garantir que um tipo específico seja sempre retornado.

Tipos de Utilitário

Os tipos de utilitário do TypeScript, como `Extract` e `Omit`, podem ser úteis ao trabalhar com uniões discriminadas. `Extract` permite extrair membros específicos de um tipo de união com base em uma condição, enquanto `Omit` permite remover propriedades de um tipo.


// Extrai o estado "sucesso" da união UIState
type SuccessState = Extract, { type: "sucesso" }>;

// Omit a propriedade 'message' da interface Error
type ErrorWithoutMessage = Omit;

Exemplos do Mundo Real em Diferentes Setores

O poder das uniões discriminadas se estende por vários setores e domínios de aplicação:

Conclusão

As uniões discriminadas em TypeScript fornecem uma maneira poderosa e com segurança de tipo de construir máquinas de estado. Ao definir claramente os estados e transições possíveis, você pode criar um código mais robusto, sustentável e compreensível. A combinação de segurança de tipo, verificação de exaustividade e preenchimento de código aprimorado torna as uniões discriminadas uma ferramenta inestimável para qualquer desenvolvedor TypeScript que lide com gerenciamento de estado complexo. Abrace as uniões discriminadas em seu próximo projeto e experimente os benefícios do gerenciamento de estado com segurança de tipo em primeira mão. Como mostramos com diversos exemplos, desde e-commerce até saúde, e logística até educação, o princípio do gerenciamento de estado com segurança de tipo por meio de uniões discriminadas é universalmente aplicável.

Se você estiver construindo um componente de UI simples ou uma aplicação empresarial complexa, as uniões discriminadas podem ajudá-lo a gerenciar o estado de forma mais eficaz e reduzir o risco de erros de tempo de execução. Então, mergulhe e explore o mundo das máquinas de estado com segurança de tipo com TypeScript!