Nederlands

Ontdek TypeScript discriminated unions, een krachtig hulpmiddel voor het bouwen van robuuste en type-veilige state machines. Leer hoe je statussen definieert, overgangen afhandelt en het typesysteem van TypeScript benut.

TypeScript Discriminated Unions: Het bouwen van type-veilige state machines

In de wereld van softwareontwikkeling is het effectief beheren van applicatiestatus cruciaal. State machines bieden een krachtige abstractie voor het modelleren van complexe stateful systemen, zorgen voor voorspelbaar gedrag en vereenvoudigen het redeneren over de logica van het systeem. TypeScript, met zijn robuuste typesysteem, biedt een fantastisch mechanisme voor het bouwen van type-veilige state machines met behulp van discriminated unions (ook wel tagged unions of algebraïsche datatypes genoemd).

Wat zijn Discriminated Unions?

Een discriminated union is een type dat een waarde representeert die een van verschillende verschillende typen kan zijn. Elk van deze typen, bekend als leden van de union, deelt een gemeenschappelijke, afzonderlijke eigenschap, de zogenaamde discriminant of tag. Deze discriminant stelt TypeScript in staat om precies te bepalen welk lid van de union momenteel actief is, wat krachtige typecontrole en automatisch aanvullen mogelijk maakt.

Zie het als een verkeerslicht. Het kan zich in een van de drie statussen bevinden: Rood, Geel of Groen. De eigenschap 'kleur' fungeert als de discriminant en vertelt ons precies in welke status het licht zich bevindt.

Waarom Discriminated Unions gebruiken voor State Machines?

Discriminated unions brengen verschillende belangrijke voordelen met zich mee bij het bouwen van state machines in TypeScript:

Een State Machine definiëren met Discriminated Unions

Laten we illustreren hoe je een state machine kunt definiëren met behulp van discriminated unions met een praktisch voorbeeld: een orderverwerkingssysteem. Een bestelling kan zich in de volgende statussen bevinden: In afwachting, In behandeling, Verzonden en Afgeleverd.

Stap 1: Definieer de statustypen

Eerst definiëren we de individuele typen voor elke status. Elk type heeft een `type` eigenschap die fungeert als de discriminant, samen met statusspecifieke gegevens.


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;
}

Stap 2: Maak het Discriminated Union Type

Vervolgens creëren we de discriminated union door deze individuele typen te combineren met behulp van de `|` (union) operator.


type OrderState = Pending | Processing | Shipped | Delivered;

Nu representeert `OrderState` een waarde die ofwel `Pending`, `Processing`, `Shipped` of `Delivered` kan zijn. De `type` eigenschap binnen elke status fungeert als de discriminant, waardoor TypeScript onderscheid kan maken tussen hen.

Statustransities afhandelen

Nu we onze state machine hebben gedefinieerd, hebben we een mechanisme nodig om tussen statussen over te gaan. Laten we een `processOrder` functie maken die de huidige status en een actie als input neemt en de nieuwe status retourneert.


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; // Geen statuswijziging

    case "processing":
      if (action.type === "shipOrder") {
        return {
          type: "shipped",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // Geen statuswijziging

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

    case "delivered":
      // Bestelling is al afgeleverd, geen verdere acties
      return state;

    default:
      // Dit zou nooit mogen gebeuren vanwege exhaustiveness checking
      return state; // Of een foutmelding genereren
  }
}

Uitleg

Exhaustiveness Checking benutten

De exhaustiveness checking van TypeScript is een krachtige functie die ervoor zorgt dat je alle mogelijke statussen in je state machine afhandelt. Als je een nieuwe status toevoegt aan de `OrderState` union maar vergeet de `processOrder` functie bij te werken, zal TypeScript een fout markeren.

Om exhaustiveness checking in te schakelen, kun je het `never` type gebruiken. Wijs binnen de `default` case van je switch statement de status toe aan een variabele van het type `never`.


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

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // Of genereer een fout
  }
}

Als het `switch` statement alle mogelijke `OrderState` waarden afhandelt, zal de `_exhaustiveCheck` variabele van het type `never` zijn en zal de code compileren. Echter, als je een nieuwe status toevoegt aan de `OrderState` union en vergeet deze af te handelen in het `switch` statement, zal de `_exhaustiveCheck` variabele van een ander type zijn en zal TypeScript een compileertijdfout genereren, waarmee je wordt gewaarschuwd voor de ontbrekende case.

Praktische Voorbeelden en Toepassingen

Discriminated unions zijn toepasbaar in een breed scala aan scenario's, verdergaand dan simpele orderverwerkingssystemen:

Voorbeeld: UI State Management

Laten we eens kijken naar een eenvoudig voorbeeld van het beheren van de status van een UI-component die gegevens ophaalt van een API. We kunnen de volgende statussen definiëren:


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 

Klik op de knop om gegevens te laden.

; case "loading": return

Laden...

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

Fout: {state.message}

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

Dit voorbeeld demonstreert hoe discriminated unions effectief kunnen worden gebruikt om de verschillende statussen van een UI-component te beheren, zodat de UI correct wordt weergegeven op basis van de huidige status. De functie `renderUI` behandelt elke status op de juiste manier en biedt een duidelijke en type-veilige manier om de UI te beheren.

Beste Praktijken voor het Gebruiken van Discriminated Unions

Om discriminated unions effectief te gebruiken in je TypeScript-projecten, overweeg dan de volgende best practices:

Geavanceerde Technieken

Conditionele Types

Conditionele types kunnen worden gecombineerd met discriminated unions om nog krachtigere en flexibelere state machines te creëren. Je kunt bijvoorbeeld conditionele types gebruiken om verschillende returntypes voor een functie te definiëren op basis van de huidige status.


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

Deze functie gebruikt een simpele `if` statement, maar kan robuuster worden gemaakt met behulp van conditionele types om ervoor te zorgen dat er altijd een specifiek type wordt geretourneerd.

Utility Types

De utility types van TypeScript, zoals `Extract` en `Omit`, kunnen nuttig zijn bij het werken met discriminated unions. `Extract` stelt je in staat om specifieke leden uit een union type te extraheren op basis van een voorwaarde, terwijl `Omit` je in staat stelt om eigenschappen uit een type te verwijderen.


// Extraheer de "success" status uit de UIState union
type SuccessState = Extract, { type: "success" }>;

// Laat de 'message' eigenschap weg uit de Error interface
type ErrorWithoutMessage = Omit;

Voorbeelden uit de Praktijk in Verschillende Industrieën

De kracht van discriminated unions strekt zich uit over verschillende industrieën en toepassingsgebieden:

Conclusie

TypeScript discriminated unions bieden een krachtige en type-veilige manier om state machines te bouwen. Door de mogelijke statussen en overgangen duidelijk te definiëren, kun je code creëren die robuuster, onderhoudbaarder en begrijpelijker is. De combinatie van typeveiligheid, exhaustiveness checking en verbeterde code-aanvulling maakt discriminated unions tot een onschatbare tool voor elke TypeScript-ontwikkelaar die te maken heeft met complex state management. Omarm discriminated unions in je volgende project en ervaar de voordelen van type-veilig state management uit de eerste hand. Zoals we hebben laten zien met diverse voorbeelden van e-commerce tot gezondheidszorg, en logistiek tot onderwijs, is het principe van type-veilig state management door middel van discriminated unions universeel toepasbaar.

Of je nu een simpele UI-component bouwt of een complexe bedrijfsapplicatie, discriminated unions kunnen je helpen om de status effectiever te beheren en het risico op runtime-fouten te verminderen. Dus duik erin en verken de wereld van type-veilige state machines met TypeScript!