TypeScript course · Part 3: The real world · lab 3 of 5
Data from outside, and modelling state
The limit of what types can promise, checking data that comes from a server, modelling the states of a feature so impossible ones cannot exist, and the escape hatches to use sparingly.
By the end you can
- Explain why a type is a claim about your code, not about your data
- Write a type guard that proves what arrived really is what you expected
- Model loading, success and failure so that impossible states cannot be written
- Know what as and the exclamation mark really do, and when they are acceptable
Easier if you did first
step 1 of 3
What types cannot promise
This is the most important idea in the course, and the one people take longest to meet.
Every type you write is checked at compile time and gone at runtime. So when you write const order = await response.json() as Order, you have not checked anything. You have told the compiler to stop asking.
If the server sends something else, that lie travels through your whole program and surfaces far away, usually as Cannot read properties of undefined, with a stack that points nowhere near the cause.
The honest shape of that line is unknown. json() genuinely returns unknown as far as your program is concerned, and unknown refuses to be used until you have proved what it is.
You prove it with a type guard: a function returning value is Order. Inside, you check the fields by hand, the same checks you would write in JavaScript. The special return type tells the compiler that a true answer means it really is an Order, so everything after the check is narrowed.
That is the pattern at every boundary: data arrives as unknown, a guard checks it, and everything inside your program is typed and trustworthy because it was actually verified once at the door.
Write the guard on the right.
function isOrder(value: unknown): value is Order {
if (typeof value !== "object" || value === null) return false;
const candidate = value as Record<string, unknown>;
return typeof candidate.id === "number" && typeof candidate.guests === "number";
}
const data: unknown = JSON.parse(text);
if (!isOrder(data)) return;
data.guests; narrowed: really an OrderWhat these words mean
- unknown
- Could be anything, and may not be used until you have checked.
- type guard
- A function returning value is Type, which narrows after a true answer.
- boundary
- Where data enters: a server, a form, storage, another library.
- as
- An assertion. It changes what the compiler believes, and checks nothing.
- schema library
- Zod and friends generate the guard and the type together, which is what real projects use.
Easy to get wrong
await response.json() as Order
Treat it as unknown and check it
The cast is a promise you cannot keep: you do not control the server. The error then appears somewhere else entirely, long after the bad data arrived.
A guard that only checks one field
Checking every field the rest of the code relies on
A half-check narrows the type completely, so the compiler stops helping while the data may still be wrong, which is worse than no guard at all.
What we check
- The compiler reports no errors
- The guard tells the compiler what a true answer means
- A real order gives its guest count
- Text where a number was expected is refused
- null and undefined are refused without throwing
- A missing menu is refused too
- You checked rather than asserting it is an Order
Nothing printed yet. Run your code.