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:
- Typeveiligheid: De compiler kan controleren of alle mogelijke statussen en overgangen correct worden afgehandeld, waardoor runtime-fouten gerelateerd aan onverwachte statustransities worden voorkomen. Dit is vooral handig in grote, complexe applicaties.
- Exhaustiveness Checking: TypeScript kan ervoor zorgen dat uw code alle mogelijke statussen van de state machine afhandelt, en u waarschuwen bij compileertijd als een status wordt gemist in een voorwaardelijke verklaring of switch-case. Dit helpt onverwacht gedrag te voorkomen en maakt uw code robuuster.
- Verbeterde Leesbaarheid: Discriminated unions definiëren duidelijk de mogelijke statussen van het systeem, waardoor de code gemakkelijker te begrijpen en te onderhouden is. De expliciete representatie van statussen verbetert de code duidelijkheid.
- Verbeterde Code Aanvulling: De intellisense van TypeScript biedt intelligente code-aanvullingssuggesties op basis van de huidige status, waardoor de kans op fouten kleiner wordt en de ontwikkeling wordt versneld.
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
- De `processOrder` functie neemt de huidige `OrderState` en een `Action` als input.
- Het gebruikt een `switch` statement om de huidige status te bepalen op basis van de `state.type` discriminant.
- Binnen elke `case` controleert het de `action.type` om te bepalen of een geldige transitie wordt geactiveerd.
- Als een geldige transitie wordt gevonden, retourneert het een nieuw statusobject met het juiste `type` en gegevens.
- Als er geen geldige transitie wordt gevonden, retourneert het de huidige status (of genereert het een fout, afhankelijk van het gewenste gedrag).
- De `default` case is opgenomen voor volledigheid en zou idealiter nooit bereikt moeten worden vanwege de exhaustiveness checking van TypeScript.
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:
- UI State Management: Het modelleren van de status van een UI-component (bijv. laden, succes, fout).
- Netwerkverzoekafhandeling: Het representeren van de verschillende fasen van een netwerkverzoek (bijv. initieel, bezig, succes, mislukking).
- Formuliervalidatie: Het volgen van de geldigheid van formuliervelden en de algehele formulierstatus.
- Game Development: Het definiëren van de verschillende statussen van een game-personage of object.
- Authenticatieflows: Het beheren van gebruikersauthenticatiestatussen (bijv. ingelogd, uitgelogd, verificatie in behandeling).
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:
- Kies Betekenisvolle Discriminant Namen: Selecteer discriminant namen die duidelijk de bedoeling van de eigenschap aangeven (bijv. `type`, `state`, `status`).
- Houd Statusgegevens Minimaal: Elke status moet alleen de gegevens bevatten die relevant zijn voor die specifieke status. Vermijd het opslaan van onnodige gegevens in statussen.
- Gebruik Exhaustiveness Checking: Schakel altijd exhaustiveness checking in om ervoor te zorgen dat je alle mogelijke statussen afhandelt.
- Overweeg het Gebruik van een State Management Bibliotheek: Overweeg voor complexe state machines het gebruik van een speciale state management bibliotheek zoals XState, die geavanceerde functies biedt zoals state charts, hiërarchische staten en parallelle staten. Voor eenvoudigere scenario's kunnen discriminated unions echter voldoende zijn.
- Documenteer je State Machine: Documenteer duidelijk de verschillende statussen, overgangen en acties van je state machine om de onderhoudbaarheid en samenwerking te verbeteren.
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:
- E-commerce (Wereldwijd): In een wereldwijd e-commerce platform kan de orderstatus worden weergegeven met discriminated unions, waarbij statussen zoals "Betaling in behandeling", "In behandeling", "Verzonden", "InTransit", "Afgeleverd" en "Geannuleerd" worden afgehandeld. Dit zorgt voor correcte tracking en communicatie in verschillende landen met verschillende verzendlogistiek.
- Financiële Diensten (Internationaal Bankieren): Het beheren van transaktiestatussen zoals "Autorisatie in behandeling", "Geautoriseerd", "In behandeling", "Voltooid", "Mislukt" is cruciaal. Discriminated unions bieden een robuuste manier om deze statussen af te handelen, in overeenstemming met diverse internationale bankvoorschriften.
- Gezondheidszorg (Remote Patient Monitoring): Het weergeven van de gezondheidstoestand van de patiënt met behulp van statussen zoals "Normaal", "Waarschuwing", "Kritiek" maakt tijdige interventie mogelijk. In wereldwijd verdeelde gezondheidszorgsystemen kunnen discriminated unions zorgen voor een consistente gegevensinterpretatie, ongeacht de locatie.
- Logistiek (Wereldwijde Supply Chain): Het volgen van de verzendstatus over internationale grenzen omvat complexe workflows. Statussen zoals "DouaneAfronding", "InTransit", "BijDistributiecentrum", "Afgeleverd" zijn perfect geschikt voor de implementatie van discriminated unions.
- Onderwijs (Online Leerplatforms): Het beheren van de inschrijvingsstatus van cursussen met statussen zoals "Ingeschreven", "InBehandeling", "Voltooid", "Gestopt" kan een gestroomlijnde leerervaring bieden, aanpasbaar aan verschillende onderwijssystemen wereldwijd.
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!