0 Tage Serie
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.