javascript / expert
Snippet
Diskriminierte Union-Typen zur Modellierung von Async-Ressourcen-Signalen
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.
snippet.js
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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;}}
angular
Erklärung
1
type ResourceState<T> = Loading | Loaded<T> | Failed;
Eine generische diskriminierte Union — die einzige Quelle der Wahrheit für jede gültige Zustandskombination, die unmögliche Zustände durch Konstruktion ausschließt.
2
@case ('loaded') { <p>{{ asLoaded(state()).data.name }}</p> }
Angulars neue Control-Flow-Syntax engt anhand des literalen `status`-Werts direkt im Template ein, analog zum Switch-Narrowing in TypeScript.
3
if (s.status !== 'loaded') throw new Error('not loaded');
Laufzeit-Guard, der das über die Template-zu-Methoden-Grenze verlorene Type-Narrowing wiederherstellt und einen Logikfehler in einen lauten Fehler statt ein stilles `undefined` verwandelt.