Runtime validation at boundaries: types do not check your API
TypeScript types disappear at runtime, so JSON from fetch can be anything. Parse it with a schema (Zod), infer types from the schema, validate env vars and forms, and know end-to-end type tools.
Comes up in: Coding, React, Frontend system design
Learn#
Rahul’s app crashes in production with toUpperCase is not a function. A backend change made plan a number. His code said const user = (await res.json()) as User, and TypeScript believed him, because types are erased when the code runs.
Runtime validation checks data while the app runs. Do it where data enters: API responses, localStorage, URL parameters, forms.
A schema describes the shape once; the library checks values against it. Zod is a popular choice:
import { z } from "zod";
const User = z.object({
id: z.number().int(),
name: z.string().min(1),
plan: z.enum(["free", "pro"]),
});
// What the API sent after a backend change: id became a string, plan was renamed.
const body = JSON.parse('{"id": "42", "name": "Asha", "tier": "pro"}');
const result = User.safeParse(body);
if (!result.success) {
for (const issue of result.error.issues) console.log(issue.path.join("."), "→", issue.message);
}
const ok = User.parse({ id: 42, name: "Asha", plan: "pro", extra: true });
console.log(ok);Output (verified)
id → Invalid input: expected number, received string
plan → Invalid option: expected one of "free"|"pro"
{ id: 42, name: 'Asha', plan: 'pro' }The extra key was removed, so the result matches its type.
Network
- GET /api/me → { id: "42", name: "Asha", tier: "pro" }
State
- body: unknown
Console
empty
fetch returns JSON. Its real type is unknown, whatever the code claims with as User.
Read the steps as text
- fetch returns JSON. Its real type is unknown, whatever the code claims with as User.
- User.safeParse checks every field against the schema at runtime.
- Parsing fails at the boundary, with a clear list of what is wrong. Nothing downstream sees bad data.
- When parsing succeeds, the result has the type z.infer<typeof User>, and unknown keys are removed.
Predict#
Predict first
const user = (await res.json()) as User. The server sends { id: '42' }. What does TypeScript do?
as is an assertion, not a check (TS.02.04), and res.json() returns any. The wrong value flows on until some later line crashes. People expect a runtime error because the type looks like a promise about the data.
Interview angle#
Say it like this — about 30 seconds
“Types are erased at runtime, so data from fetch, storage, URLs or forms is really unknown. I parse it at the boundary with a schema library such as Zod. The schema checks the value, returns typed data or a list of errors, and I infer the TypeScript type from the schema, so there is one source of truth. Parse, don’t validate: after parsing, the rest of the app trusts the type. For full-stack TypeScript, tRPC or OpenAPI generators share types end to end.”
Follow-up What does parse, don't validate mean?
Follow-up How do you avoid writing the type and the schema twice?
type User = z.infer<typeof User>.Follow-up Where else should you validate?
postMessage and workers, and saved data after an app update.Practice#
On your own
Write parseProduct by hand, the way a schema library works inside: check every field, collect every problem, and return only known keys.
Done when
- A valid product is returned with only
sku,priceandtags. - Every problem is listed in one error message, joined with ”; ”.
- Non-objects and
nullthrow “expected an object”.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
Why can't TypeScript types check API responses?
Types are erased at runtime; the data is whatever the server sent.
How do you get a TypeScript type from a Zod schema?
type User = z.infer<typeof User>
parse vs safeParse in Zod?
parse throws on bad data; safeParse returns a success flag with data or error.
What does parse, don't validate mean?
Convert untrusted input into a trusted, precise type once at the boundary.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
Treat res.json() as unknown and pass it to a parser. This is parse, don’t validate: check once at the edge and get back a value whose type you can trust. unknown forces a check before use; any and as skip it (TS.01.03).
Check yourself: What type should res.json() really have in your code?
unknown, until it is parsed.
How it works underneathSenior rounds
z.infer<typeof Schema> gives the output type, so the schema is the single source of truth.Check yourself: What does Zod do with keys that are not in an object schema, by default?
Removes them from the output.
Edge cases and trapsWhere people slip
Check yourself: Why validate environment variables at startup?
A missing or wrong value then fails at once, not deep inside a request.
In productionStaff-level answers
Check yourself: Name two ways to share API types between server and client.
tRPC, or generating clients and types from an OpenAPI spec.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.