Eine häufige Quelle von Laufzeitfehlern in mehrstufigen Angular-Formularen ist ein einziges flaches Interface, in dem jedes Feld optional ist (cardToken?: string, orderId?: string) — das lässt eine Komponente problemlos kompilieren, obwohl sie ein Feld liest, das im aktuellen Schritt logisch noch gar nicht existieren kann, und liefert stillschweigend undefined statt eines Kompilierfehlers. Wird der Wizard als TypeScript-Discriminated-Union modelliert, die über ein step-Literal unterschieden wird, werden ungültige Zustände unrepräsentierbar: Der Typprüfer verweigert es submitAccount, einen 'confirm'-Zustand ohne ein agreed-Feld zu konstruieren, und jede Komponente, die state() liest, muss vor dem Zugriff auf schrittspezifische Felder über .step einengen — das fängt eine ganze Klasse von Null-Reference-Bugs schon beim Build statt erst in Produktion ab. Dieses Muster skaliert sauber auf signal-basierte Stores, ohne für mäßig komplexe Abläufe eine vollständige externe State-Machine-Bibliothek wie XState zu benötigen.
type WizardState =| { step: 'account'; email: string }| { step: 'billing'; email: string; cardToken: string }| { step: 'confirm'; email: string; cardToken: string; agreed: boolean }| { step: 'done'; orderId: string };import { Injectable, signal } from '@angular/core';@Injectable({ providedIn: 'root' })export class CheckoutWizardStore {private readonly state = signal<WizardState>({step: 'account',email: '',});readonly current = this.state.asReadonly();submitAccount(email: string): void {// The compiler forbids constructing a 'billing' state without// the fields that 'account' never had, e.g. cardToken.this.state.set({ step: 'billing', email, cardToken: '' });}submitBilling(cardToken: string): void {const s = this.state();if (s.step !== 'billing') {throw new Error(`Cannot submit billing from step '${s.step}'`);}this.state.set({ ...s, step: 'confirm', cardToken, agreed: false });}confirmOrder(orderId: string): void {const s = this.state();if (s.step !== 'confirm' || !s.agreed) {throw new Error('Order cannot be confirmed yet');}this.state.set({ step: 'done', orderId });}}