Statt `loading`, `data` und `error` als drei separate optionale Felder zu führen (was unmögliche Kombinationen wie loading=true mit gesetztem data erlaubt), wird die Ressource als diskriminierte Union über ein literales `status`-Feld modelliert. Durch TypeScripts Control-Flow-Narrowing sind innerhalb jedes `@switch`-Zweigs nur die für diesen Zustand gültigen Felder zugreifbar — der Compiler verhindert physisch den Zugriff auf `.data`, solange `status` gleich `'loading'` ist. Die Hilfsfunktionen `asLoaded`/`asError` führen eine Laufzeit-Assertion aus, die das Compile-Time-Narrowing nachbildet, da Template-Ausdrücke das Narrowing über Methodenaufrufe hinweg nicht wie Inline-Code beibehalten.
type Loading = { status: 'loading' };type Loaded<T> = { status: 'loaded'; data: T };type Failed = { status: 'error'; error: string };type ResourceState<T> = Loading | Loaded<T> | Failed;@Component({selector: 'app-user-panel',standalone: true,template: `@switch (state().status) {@case ('loading') { <p>Loading…</p> }@case ('loaded') { <p>{{ asLoaded(state()).data.name }}</p> }@case ('error') { <p class="err">{{ asError(state()).error }}</p> }}`,})export class UserPanelComponent {private readonly userId = input.required<string>();private readonly userService = inject(UserService);readonly state = computed<ResourceState<User>>(() => {const result = this.userService.fetchState(this.userId());return result;});asLoaded(s: ResourceState<User>): Loaded<User> {if (s.status !== 'loaded') throw new Error('not loaded');return s;}asError(s: ResourceState<User>): Failed {if (s.status !== 'error') throw new Error('not an error state');return s;}}