Lesson 5 of 30
A TypeScript type on a fetch response is a promise checked once, at compile time, by a human — nothing verifies at runtime that the data actually arriving matches it, and a silent backend change slips right through.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A function is typed as `Promise<User>` and simply returns `res.json()`. The backend renames a field the type expects. What happens?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Writing const data: User = await response.json() doesn't check anything. TypeScript trusts that type annotation completely and never looks at it again once compilation finishes — there's no code running at runtime that confirms the JSON that actually arrived has the fields, types, or shape the annotation claims. If a backend renames a field, makes a field that used to always be present now sometimes missing, or changes a field from a string to a number, TypeScript will not catch it, because TypeScript never runs against the real response at all.
The fix is runtime schema validation: a library that actually parses the raw response data and checks it against a schema while the program is running, throwing or returning a typed error if the real shape doesn't match what was expected. This repo's own codebase already uses Zod for exactly this purpose — it's a concrete, reasonable library to name when this comes up, though the same idea applies to any runtime validation library.
// Compile-time-only "safety" — this is just an assertion, not a check
async function getUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json(); // TypeScript trusts this is a User. Nothing verifies it.
}
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
email: z.string(),
displayName: z.string(),
});
type User = z.infer<typeof UserSchema>;
async function getUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const raw = await res.json();
return UserSchema.parse(raw); // actually checked against the real response, at runtime
}
If the backend renames displayName to name tomorrow, the first version keeps compiling and keeps "working" — every consumer of user.displayName silently gets undefined, and the bug surfaces wherever that value happens to get rendered or used, possibly far from the actual fetch call. The second version fails loudly, at the fetch call itself, with a clear message about exactly which field didn't match — the moment the contract breaks, not sometime later downstream.
undefined and propagates. Without runtime validation, accessing a property that no longer exists on the response doesn't crash where the bad data entered the app; it silently becomes undefined and often crashes (or worse, silently misbehaves) somewhere unrelated and much harder to trace back."How do you guard against a backend response shape changing unexpectedly?" is a real, common question, and naming runtime schema validation specifically (not just "we have TypeScript types") — plus being able to articulate that TypeScript types alone provide zero runtime guarantee — shows a candidate understands the actual gap between compile-time and runtime safety, not just that a validation library exists.