Code

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 just message?? Because this way the type guarantees: if the state is error, a message MUST be there. With message? 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".