Italiano

Esplora le unioni discriminate di TypeScript, un potente strumento per costruire macchine a stati robuste e type-safe. Impara a definire stati, gestire transizioni e sfruttare il sistema di tipi di TypeScript per una maggiore affidabilità del codice.

Unioni discriminate TypeScript: costruzione di macchine a stati type-safe

Nel campo dello sviluppo software, la gestione dello stato dell'applicazione in modo efficace è fondamentale. Le macchine a stati forniscono un'astrazione potente per modellare sistemi complessi con stati, garantendo un comportamento prevedibile e semplificando il ragionamento sulla logica del sistema. TypeScript, con il suo solido sistema di tipi, offre un meccanismo fantastico per costruire macchine a stati type-safe utilizzando le unioni discriminate (note anche come unioni taggate o tipi di dati algebrici).

Cosa sono le unioni discriminate?

Un'unione discriminata è un tipo che rappresenta un valore che può essere uno di diversi tipi diversi. Ciascuno di questi tipi, noti come membri dell'unione, condivide una proprietà comune e distinta chiamata discriminante o tag. Questo discriminante consente a TypeScript di determinare con precisione quale membro dell'unione è attualmente attivo, consentendo potenti controlli di tipo e completamento automatico.

Pensa a un semaforo. Può essere in uno dei tre stati: rosso, giallo o verde. La proprietà 'colore' funge da discriminante, dicendoci esattamente in quale stato si trova la luce.

Perché usare le unioni discriminate per le macchine a stati?

Le unioni discriminate apportano diversi vantaggi chiave quando si costruiscono macchine a stati in TypeScript:

Definizione di una macchina a stati con unioni discriminate

Illustriamo come definire una macchina a stati utilizzando le unioni discriminate con un esempio pratico: un sistema di elaborazione degli ordini. Un ordine può essere nei seguenti stati: In attesa, In elaborazione, Spedito e Consegnato.

Passaggio 1: definire i tipi di stato

Innanzitutto, definiamo i singoli tipi per ogni stato. Ogni tipo avrà una proprietà `type` che funge da discriminante, insieme a qualsiasi dato specifico dello stato.


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

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

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

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

Passaggio 2: creare il tipo di unione discriminata

Successivamente, creiamo l'unione discriminata combinando questi singoli tipi utilizzando l'operatore `|` (unione).


type OrderState = Pending | Processing | Shipped | Delivered;

Ora, `OrderState` rappresenta un valore che può essere `Pending`, `Processing`, `Shipped` o `Delivered`. La proprietà `type` all'interno di ogni stato funge da discriminante, consentendo a TypeScript di differenziarli.

Gestione delle transizioni di stato

Ora che abbiamo definito la nostra macchina a stati, abbiamo bisogno di un meccanismo per passare da uno stato all'altro. Creiamo una funzione `processOrder` che accetta lo stato corrente e un'azione come input e restituisce il nuovo stato.


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

function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    case "pending":
      if (action.type === "startProcessing") {
        return {
          type: "processing",
          orderId: state.orderId,
          assignedAgent: action.payload.agentId,
        };
      }
      return state; // Nessun cambio di stato

    case "processing":
      if (action.type === "shipOrder") {
        return {
          type: "shipped",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // Nessun cambio di stato

    case "shipped":
      if (action.type === "deliverOrder") {
        return {
          type: "delivered",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // Nessun cambio di stato

    case "delivered":
      // L'ordine è già stato consegnato, nessuna azione ulteriore
      return state;

    default:
      // Questo non dovrebbe mai accadere a causa del controllo di completezza
      return state; // Oppure lancia un errore
  }
}

Spiegazione

Sfruttare il controllo di completezza

Il controllo di completezza di TypeScript è una potente funzionalità che garantisce la gestione di tutti i possibili stati nella tua macchina a stati. Se aggiungi un nuovo stato all'unione `OrderState` ma ti dimentichi di aggiornare la funzione `processOrder`, TypeScript segnalerà un errore.

Per abilitare il controllo di completezza, puoi usare il tipo `never`. All'interno del `default` case della tua istruzione switch, assegna lo stato a una variabile di tipo `never`.


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (casi precedenti) ...

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

Se l'istruzione `switch` gestisce tutti i possibili valori `OrderState`, la variabile `_exhaustiveCheck` sarà di tipo `never` e il codice verrà compilato. Tuttavia, se aggiungi un nuovo stato all'unione `OrderState` e dimentichi di gestirlo nell'istruzione `switch`, la variabile `_exhaustiveCheck` sarà di un tipo diverso e TypeScript genererà un errore in fase di compilazione, avvisandoti del caso mancante.

Esempi pratici e applicazioni

Le unioni discriminate sono applicabili in un'ampia gamma di scenari oltre ai semplici sistemi di elaborazione degli ordini:

Esempio: gestione dello stato dell'interfaccia utente

Consideriamo un semplice esempio di gestione dello stato di un componente dell'interfaccia utente che recupera dati da un'API. Possiamo definire i seguenti stati:


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 

Clicca il pulsante per caricare i dati.

; case "loading": return

Caricamento...

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

Errore: {state.message}

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

Questo esempio dimostra come le unioni discriminate possono essere utilizzate per gestire efficacemente i diversi stati di un componente dell'interfaccia utente, garantendo che l'interfaccia utente venga renderizzata correttamente in base allo stato corrente. La funzione `renderUI` gestisce ogni stato in modo appropriato, fornendo un modo chiaro e type-safe per gestire l'interfaccia utente.

Best practice per l'utilizzo delle unioni discriminate

Per utilizzare efficacemente le unioni discriminate nei tuoi progetti TypeScript, considera le seguenti best practice:

Tecniche avanzate

Tipi condizionali

I tipi condizionali possono essere combinati con le unioni discriminate per creare macchine a stati ancora più potenti e flessibili. Ad esempio, puoi usare i tipi condizionali per definire diversi tipi di ritorno per una funzione in base allo stato corrente.


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

Questa funzione utilizza una semplice istruzione `if`, ma potrebbe essere resa più solida usando i tipi condizionali per garantire che venga sempre restituito un tipo specifico.

Tipi di utilità

I tipi di utilità di TypeScript, come `Extract` e `Omit`, possono essere utili quando si lavora con le unioni discriminate. `Extract` ti consente di estrarre membri specifici da un tipo di unione in base a una condizione, mentre `Omit` ti consente di rimuovere le proprietà da un tipo.


// Estrai lo stato "success" dall'unione UIState
type SuccessState = Extract, { type: "success" }>;

// Ometti la proprietà 'message' dall'interfaccia Error
type ErrorWithoutMessage = Omit;

Esempi reali in diversi settori

La potenza delle unioni discriminate si estende a vari settori e domini applicativi:

Conclusione

Le unioni discriminate TypeScript forniscono un modo potente e type-safe per costruire macchine a stati. Definendo chiaramente i possibili stati e transizioni, puoi creare un codice più robusto, mantenibile e comprensibile. La combinazione di type safety, controllo di completezza e completamento del codice migliorato rende le unioni discriminate uno strumento prezioso per qualsiasi sviluppatore TypeScript che si occupa della gestione di stati complessi. Abbraccia le unioni discriminate nel tuo prossimo progetto e sperimenta in prima persona i vantaggi della gestione degli stati type-safe. Come abbiamo dimostrato con diversi esempi dall'e-commerce all'assistenza sanitaria, e dalla logistica all'istruzione, il principio della gestione degli stati type-safe attraverso le unioni discriminate è universalmente applicabile.

Che tu stia costruendo un semplice componente dell'interfaccia utente o un'applicazione aziendale complessa, le unioni discriminate possono aiutarti a gestire lo stato in modo più efficace e ridurre il rischio di errori di runtime. Quindi, immergiti ed esplora il mondo delle macchine a stati type-safe con TypeScript!