A practice prompt we wrote. No company or candidate report names it, so it carries no company tag.

How to answer

Start from the data, not the syntax: a table of which fields exist in which state, turned into types that make every other combination a compile error. It all needs strict: true: with strictNullChecks off, a required field accepts null.

  1. Get real payloads. First ask: the payload, or the client’s request state? isLoading, error and data as three fields allow all three at once; { state: "loading" } | { state: "error"; error } | { state: "ok"; data } fixes it the same way. For the payload, get one example per state and say the table: result only on success, error only on failure, progress only while running.
  2. Write a discriminated union. Not one type with every field optional, which allows any mix, but one variant per state, keyed on a string literal: { status: "succeeded"; result: Report }, { status: "failed"; error: { code: ErrorCode; retryable: boolean } }, all fields required.
  3. Make handling exhaustive. Switch on status and pass default to assertNever(x: never): never, so a new variant breaks the build at every switch that forgot it. The TypeScript handbook shows the same check as an assignment to a never variable.
  4. Validate at the boundary. Types vanish at runtime. TypeScript’s DOM typings declare res.json() as Promise<any>, so it assigns to any type without complaint; Node’s own typings say Promise<unknown>, which forces a cast, and a cast checks nothing. Treat it as unknown and parse it once. With Zod, derive the type from the schema, type Job = z.infer<typeof Job>, so validator and type cannot drift; safeParse returns the value or the issues. After that line, the code trusts the type.
  5. Decide what an unknown status does. A vendor can add one without notice. Reject it loudly, or map it to { status: "unrecognized"; raw: unknown }, shown as “check back later”. Pick one and say why.

The trap is as ApiResponse, or no cast at all where any needs none. It compiles, the types lie, and the crash surfaces three screens from the bad payload.

The typed responses drill runs this against hidden tests, including a payload that must be rejected at runtime.

Follow-ups

What the interviewer may ask next, once your first answer is on the table.

  • The API adds a new status next month. What happens in your client at compile time, and what happens at runtime before you redeploy?
  • How do you stop someone passing a customer ID where an order ID belongs, when both are strings?
  • Where does validation happen, and what may the rest of the code assume after it?
  • Would you generate these types from the vendor’s OpenAPI spec? What would you still write by hand?

Where answers go wrong

  • Casting await res.json() with as, or assigning it to a typed variable, which compiles with no cast wherever the DOM typings are loaded, because they declare res.json() as Promise<any>. Either way the types describe the payload you hoped for rather than the one that arrived.
  • One interface with every field optional, so “succeeded with no result” and “failed with a result” both type-check.
  • A switch with a default that quietly returns, so a new variant compiles and falls through unhandled.

Answer this in two minutes

Write the answer you would say out loud. The clock starts with your first word.

Two minutes

Model answer

“Can I see real payloads first, one per state? The job API you showed me has four states, and the fields depend on the state. result exists only when the job succeeded, error only when it failed, and progress only while it’s running.