Engrain by ByteRush

TS.08.04 Mid-level· 11 minCore — asked often

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);
This example uses Node.js, so it runs in our checks rather than in your browser.

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.

Parsing at the boundaryStep 1 of 4

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
    1. fetch returns JSON. Its real type is unknown, whatever the code claims with as User.
    2. User.safeParse checks every field against the schema at runtime.
    3. Parsing fails at the boundary, with a clear list of what is wrong. Nothing downstream sees bad data.
    4. 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?

    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.”

    Stays on your device. Nothing is uploaded.
    Follow-up What does parse, don't validate mean?
    Turn untrusted input into a value of a more precise type once, at the edge, instead of checking it again in many places.
    Follow-up How do you avoid writing the type and the schema twice?
    Infer the type: type User = z.infer<typeof User>.
    Follow-up Where else should you validate?
    Environment variables at startup, form input before sending, messages from 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, price and tags.
    • Every problem is listed in one error message, joined with ”; ”.
    • Non-objects and null throw “expected an object”.
    Your solution
    Ctrl/⌘ + Enter

    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.
    Parsing returns a new value: unknown keys are stripped by default, and transforms can convert strings to dates or numbers Source: Zod: Defining schemas.

    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

    Validate env vars when the app starts, so a missing key fails the deploy, not a user’s request. Reuse one schema for a form on the client and for the same data on the server.
    Bundle size matters on the client: check the library’s size, and consider a smaller one such as Valibot.

    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
    For end-to-end types, tRPC shares a TypeScript router between server and client, and OpenAPI generators create typed clients from an API spec Source: tRPC docs. Still parse at real trust boundaries, such as third-party APIs.
    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.