Lesson 64 of 80
JSON.parse returns any, which means whatever you assign it to is trusted with zero checking — a JsonValue type and real narrowing close that hole.
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.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →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
JSON.parse has the type signature (text: string) => any. That's not a small detail — any at a boundary doesn't just leave that one value untyped, it turns off type checking for everything that value touches next. Assign the result to a variable typed User, and TypeScript believes you completely, no matter what's actually inside the string. This is the any vs unknown lesson's core warning showing up at one of the most common real boundaries in an app: data that crossed the network.
The fix has two parts. First, describe what JSON can actually contain with a real type — a JsonValue — so "arbitrary JSON" isn't the same as "no type at all." Second, treat anything you get back from JSON.parse as that JsonValue, not as your app's real type, until something has actually checked its shape.
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };
function parseJson(text: string): JsonValue {
return JSON.parse(text) as JsonValue; // one narrow, deliberate assertion, at the one true boundary
}
interface User {
id: number;
name: string;
}
function isUser(value: JsonValue): value is User {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
typeof value.id === "number" &&
typeof value.name === "string"
);
}
function loadUser(text: string): User {
const data = parseJson(text);
if (!isUser(data)) {
throw new Error("response did not match the User shape");
}
return data; // narrowed to User here — genuinely checked, not assumed
}
JsonValue is recursive — an object's values and an array's elements are themselves JsonValue, all the way down — which is the honest shape of "any valid JSON document." parseJson casts exactly once, at the one place the type information is genuinely unknowable from static analysis alone. Everywhere after that, isUser does real work: it's a type guard (from the type guards lesson) that actually inspects the value before TypeScript will let you treat it as a User.
as JsonValue after JSON.parse doesn't check anything — it's still just an assertion. The safety doesn't come from the cast; it comes from isUser actually inspecting the value afterward. Skip the guard and you're back to trusting the network blindly, just with an extra type name in between.unknown alone isn't a JSON type. unknown says "could be literally anything, including a function or a class instance" — JsonValue is narrower and more honest: "could be anything JSON can actually encode." Use JsonValue when you specifically mean parsed JSON, unknown for a truly unconstrained boundary.JsonValue correct for real payloads. A shallow version — string | number | boolean | null | object — technically compiles but the bare object swallows nested shapes and gives you nothing to narrow against a few levels down. The self-referencing array and index-signature cases aren't optional extras."JSON.parse returns any — what's actually wrong with that, and how would you fix it?" checks whether a candidate understands that any isn't just "untyped," it's contagious through everything downstream — and whether they can name the concrete fix (a JsonValue type plus a real narrowing step) instead of just casting straight to the shape they hoped for.