Theory

The hole: types don't check what you got

In lesson 16 we wrote types, and the compiler started protecting us. But there's a hole — and it's important to understand it, so you don't trust types blindly.

response.json() returns any. Think about what that means:

const data = await response.json();   // data: any
const pics: Apod[] = data;            // ✅ the compiler is happy... but WHY?

any turns off type checking. The compiler believes data is Apod[] because you said so. But it never actually checked. If NASA changes the response shape tomorrow (renames url to image_url), the types say nothing — the app breaks at runtime while TypeScript stays silent.

This is the key idea: types describe what you EXPECT; they don't check what you GOT. TypeScript won't save you from an API that changed, from corrupt JSON, or from a null where you expected an object. Types are a compile-time promise, not a runtime shield.

The fix — validate at the BOUNDARY. At the point where untrusted data enters the app (a network response, localStorage, the URL), we write a type guard:

function isApod(value: unknown): value is Apod { /* checks the fields */ }

The return type value is Apod is special. It tells the compiler: "if this function returned true, you may treat value as an Apod." That's how untrusted unknown becomes a trusted type — but only after a real, runtime check.

And two related ideas we'll add:

  • Readonly<AppState> — views can read state but not change it. The only way to change it is an action (like lesson 9, now enforced).
  • catch (error) is unknown, not any — because JavaScript allows throw 42. unknown forces you to narrow before using it.