Svenska

Utforska TypeScript diskriminerade unioner, ett kraftfullt verktyg för att bygga robusta och typsäkra tillståndsmaskiner. Lär dig definiera tillstånd och hantera övergångar.

TypeScript Diskriminerade Unioner: Skapa Typsäkra Tillståndsmaskiner

Inom mjukvaruutveckling är det avgörande att hantera applikationstillstånd effektivt. Tillståndsmaskiner ger en kraftfull abstraktion för att modellera komplexa tillståndskänsliga system, vilket säkerställer förutsägbart beteende och förenklar resonemang kring systemets logik. TypeScript, med sitt robusta typsystem, erbjuder en fantastisk mekanism för att bygga typsäkra tillståndsmaskiner med hjälp av diskriminerade unioner (även kända som taggade unioner eller algebraiska datatyper).

Vad är Diskriminerade Unioner?

En diskriminerad union är en typ som representerar ett värde som kan vara en av flera olika typer. Var och en av dessa typer, kända som medlemmar i unionen, delar en gemensam, distinkt egenskap som kallas diskriminant eller tagg. Denna diskriminant tillåter TypeScript att exakt bestämma vilken medlem av unionen som är aktiv, vilket möjliggör kraftfull typkontroll och autokomplettering.

Tänk på det som ett trafikljus. Det kan vara i ett av tre tillstånd: Rött, Gult eller Grönt. Egenskapen "color" fungerar som diskriminant och talar om exakt vilket tillstånd ljuset är i.

Varför Använda Diskriminerade Unioner för Tillståndsmaskiner?

Diskriminerade unioner ger flera viktiga fördelar när man bygger tillståndsmaskiner i TypeScript:

Definiera en Tillståndsmaskin med Diskriminerade Unioner

Låt oss illustrera hur man definierar en tillståndsmaskin med hjälp av diskriminerade unioner med ett praktiskt exempel: ett orderhanteringssystem. En order kan vara i följande tillstånd: Väntande, Bearbetning, Skickad och Levererad.

Steg 1: Definiera Tillståndstyperna

Först definierar vi de enskilda typerna för varje tillstånd. Varje typ kommer att ha en `type`-egenskap som fungerar som diskriminant, tillsammans med all tillståndsspecifik data.


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

Steg 2: Skapa den Diskriminerade Unionstypen

Därefter skapar vi den diskriminerade unionen genom att kombinera dessa individuella typer med hjälp av operatorn `|` (union).


type OrderState = Pending | Processing | Shipped | Delivered;

Nu representerar `OrderState` ett värde som kan vara antingen `Pending`, `Processing`, `Shipped` eller `Delivered`. Egenskapen `type` i varje tillstånd fungerar som diskriminanten, vilket tillåter TypeScript att skilja mellan dem.

Hantera Tillståndsövergångar

Nu när vi har definierat vår tillståndsmaskin behöver vi en mekanism för att övergå mellan tillstånd. Låt oss skapa en `processOrder`-funktion som tar det aktuella tillståndet och en åtgärd som indata och returnerar det nya tillståndet.


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; // Ingen tillståndsändring

    case "processing":
      if (action.type === "shipOrder") {
        return {
          type: "shipped",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // Ingen tillståndsändring

    case "shipped":
      if (action.type === "deliverOrder") {
        return {
          type: "delivered",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // Ingen tillståndsändring

    case "delivered":
      // Ordern är redan levererad, inga ytterligare åtgärder
      return state;

    default:
      // Detta borde aldrig hända på grund av uttömmande kontroll
      return state; // Eller kasta ett fel
  }
}

Förklaring

Utnyttja Uttömmande Kontroll

TypeScript:s uttömmande kontroll är en kraftfull funktion som säkerställer att du hanterar alla möjliga tillstånd i din tillståndsmaskin. Om du lägger till ett nytt tillstånd i `OrderState`-unionen men glömmer att uppdatera funktionen `processOrder` kommer TypeScript att flagga ett fel.

För att aktivera uttömmande kontroll kan du använda typen `never`. Inuti `default`-fallet i din switch-sats, tilldela tillståndet till en variabel av typen `never`.


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (tidigare fall) ...

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // Eller kasta ett fel
  }
}

Om `switch`-satsen hanterar alla möjliga `OrderState`-värden kommer variabeln `_exhaustiveCheck` att vara av typen `never` och koden kommer att kompileras. Men om du lägger till ett nytt tillstånd i `OrderState`-unionen och glömmer att hantera det i `switch`-satsen kommer variabeln `_exhaustiveCheck` att vara av en annan typ, och TypeScript kommer att kasta ett kompileringsfel, vilket varnar dig om det saknade fallet.

Praktiska Exempel och Applikationer

Diskriminerade unioner är tillämpliga i en mängd olika scenarier utöver enkla orderhanteringssystem:

Exempel: UI-tillståndshantering

Låt oss betrakta ett enkelt exempel på att hantera tillståndet för en UI-komponent som hämtar data från ett API. Vi kan definiera följande tillstånd:


interface Initial {
  type: "initial";
}

interface Loading {
  type: "loading";
}

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

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

type UIState<T> = Initial | Loading | Success<T> | Error;

function renderUI<T>(state: UIState<T>): React.ReactNode {
  switch (state.type) {
    case "initial":
      return <p>Klicka på knappen för att ladda data.</p>;
    case "loading":
      return <p>Laddar...</p>;
    case "success":
      return <pre>{JSON.stringify(state.data, null, 2)}</pre>;
    case "error":
      return <p>Fel: {state.message}</p>;
    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck;
  }
}

Detta exempel visar hur diskriminerade unioner kan användas för att effektivt hantera de olika tillstånden för en UI-komponent, vilket säkerställer att gränssnittet renderas korrekt baserat på det aktuella tillståndet. Funktionen `renderUI` hanterar varje tillstånd på lämpligt sätt, vilket ger ett tydligt och typsäkert sätt att hantera gränssnittet.

Bästa Metoder för att Använda Diskriminerade Unioner

För att effektivt utnyttja diskriminerade unioner i dina TypeScript-projekt, överväg följande bästa metoder:

Avancerade Tekniker

Villkorliga Typer

Villkorliga typer kan kombineras med diskriminerade unioner för att skapa ännu kraftfullare och flexiblare tillståndsmaskiner. Till exempel kan du använda villkorliga typer för att definiera olika returtyper för en funktion baserat på det aktuella tillståndet.


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

Denna funktion använder en enkel `if`-sats men kan göras mer robust med hjälp av villkorliga typer för att säkerställa att en specifik typ alltid returneras.

Utility Typer

TypeScript:s utility typer, såsom `Extract` och `Omit`, kan vara användbara när du arbetar med diskriminerade unioner. `Extract` tillåter dig att extrahera specifika medlemmar från en unionstyp baserat på ett villkor, medan `Omit` tillåter dig att ta bort egenskaper från en typ.


// Extrahera "success"-tillståndet från UIState-unionen
type SuccessState<T> = Extract<UIState<T>, { type: "success" }>;

// Utelämna egenskapen 'message' från Error-interfacet
type ErrorWithoutMessage = Omit<Error, "message">;

Verkliga Exempel Inom Olika Branscher

Kraften hos diskriminerade unioner sträcker sig över olika branscher och applikationsområden:

Slutsats

TypeScript diskriminerade unioner ger ett kraftfullt och typsäkert sätt att bygga tillståndsmaskiner. Genom att tydligt definiera de möjliga tillstånden och övergångarna kan du skapa mer robust, underhållbar och begriplig kod. Kombinationen av typsäkerhet, uttömmande kontroll och förbättrad kodkomplettering gör diskriminerade unioner till ett ovärderligt verktyg för alla TypeScript-utvecklare som hanterar komplex tillståndshantering. Anamma diskriminerade unioner i ditt nästa projekt och upplev fördelarna med typsäker tillståndshantering på egen hand. Som vi har visat med olika exempel från e-handel till hälsovård och logistik till utbildning, är principen om typsäker tillståndshantering genom diskriminerade unioner universellt tillämplig.

Oavsett om du bygger en enkel UI-komponent eller en komplex företagsapplikation kan diskriminerade unioner hjälpa dig att hantera tillstånd mer effektivt och minska risken för runtime-fel. Så dyk in och utforska världen av typsäkra tillståndsmaskiner med TypeScript!