src/types.ts — state as a union
In the code column — the state types, added to src/types.ts. This is where the most powerful idea lives.
LoadStatus as a union:
export type LoadStatus =
| { state: 'idle' }
| { state: 'loading' }
| { state: 'loaded' }
| { state: 'error'; message: string; retryable: boolean };
Compare it to the vanilla version: there isLoading (a boolean) and error (a string) were separate. Nothing stopped:
isLoading = true;
error = 'Network failed'; // both at once — what do you render?!
The UI rendered that as nonsense (a spinner AND an error). In the union that combination doesn't exist: LoadStatus is either loading or error — never both. And the error variant forces a message and retryable; you can't have "an error with no message".
AppState — the whole state in one type (instead of the vanilla dozen loose lets). Notice saved: SavedPicture[], where SavedPicture has rating: Rating — so a rating can only be 0–5.
How you use it (narrowing on state):
switch (status.state) {
case 'loading': showSpinner(); break;
case 'error': showError(status.message); break; // message is GUARANTEED here
case 'loaded': render(); break;
case 'idle': break;
}
status.message is reachable only in the error branch — elsewhere it doesn't exist, and the compiler knows it.
Tip. Why
{ state: 'error'; message; retryable }and not justmessage?? Because this way the type guarantees: if the state is error, a message MUST be there. Withmessage?you could have "an error with no message" and render a blank screen again. The type encodes not just "which fields are possible", but "which fields belong together".