skip to content

You describe an API response in TypeScript by remapping its snake_case keys to camelCase with a mapped type's `as` clause. What does that type actually guarantee about the object you receive at runtime, and how do you close the gap?

level: seniorimportance: should knowfreq 26%

answer

  1. the type layer emits nothing
  2. two different meanings of `as`
  3. the payload keeps its own keys
  4. a mapper, not a mapped type
  5. validate before you believe the shape

basics

~20 s

Nothing. A key-remapped type is erased at compile time, so the received object still has its original snake_case keys. You close the gap with a real mapping function whose return type is the remapped type, plus validation of the payload at the boundary.

solid answer

~50 s

It guarantees nothing about the payload. A mapped type is a description that the compiler erases — it emits no code, so no key is renamed while the program runs. If you assert the response into the camelCase type, `response.userId` is `undefined` at runtime while the checker insists it is a `number`. The fix has two halves. First, write an actual mapper: `function toDomain(w: Wire): Camel<Wire>`, and let the remapped type be its *return* type, so the compiler checks the function against the wire shape and a new or renamed wire field breaks the build instead of leaking through. Second, remember the wire type is itself an assumption — `JSON.parse` is typed to return `any` — so validate the payload at the boundary before trusting either shape. The remapping is the contract; the mapper and the validator are the enforcement.

code

typescript · 11 lines
typescript
type Wire = { user_id: number; created_at: string };
type Names = { user_id: "userId"; created_at: "createdAt" };

type Domain = { [K in keyof Wire as Names[K]]: Wire[K] };

// The remapped type is the mapper's contract, not a conversion.
function toDomain(w: Wire): Domain {
  return { userId: w.user_id, createdAt: w.created_at };
}

const domain = toDomain({ user_id: 1, created_at: "2024-01-01" });

go deeper

for a junior

Remember that TypeScript types vanish when the code is compiled, so a type describing camelCase keys cannot make a JSON response have them. Say that plainly and you have the core of the answer.

for a middle

Explain the mechanism: mapped types emit no code, and a type assertion performs no check, so the mismatch surfaces as undefined at runtime rather than as an error at the assertion.

for a senior

Demonstrate the repair as a pair — a mapper whose return type is derived from the wire type so drift becomes a build error, plus boundary validation because the wire type is itself an unverified claim.

for a principal

Own the boundary policy: whether the codebase renames at all, where the single translation point lives, and how you stop half-renamed vocabularies from spreading across modules.

## The one rule this question tests TypeScript's type layer is erased. Compilation removes annotations, interfaces, generics and mapped types and emits plain JavaScript. A key-remapped type is therefore a statement *about* data, never an operation *on* data. `{ [K in keyof Wire as Names[K]]: Wire[K] }` does not rename anything; it names a shape you would like some value to have. That makes the failure mode specific and common. Someone writes the elegant remapping, then reaches for the other `as` — the type assertion — to make the fetch result fit: ```ts const user = (await res.json()) as Domain; // compiles, and is a lie user.userId; // typed number, actually undefined ``` The assertion performs no check and produces no conversion. The runtime object still has `user_id`. Every downstream read of `userId` yields `undefined`, and the checker will happily let you do arithmetic on it. ## The mapper is the missing half The repair is a function that really moves the fields, with the remapped type as its declared return type: ```ts type Wire = { user_id: number; created_at: string }; type Names = { user_id: "userId"; created_at: "createdAt" }; type Domain = { [K in keyof Wire as Names[K]]: Wire[K] }; function toDomain(w: Wire): Domain { return { userId: w.user_id, createdAt: w.created_at }; } ``` Now the type earns its keep. Because `Domain` is *derived* from `Wire`, the compiler cross-checks the two: add a field to `Wire` and `toDomain` stops compiling because the returned object is missing the new derived key; rename a wire field and the same thing happens. That is exactly the drift you would otherwise discover in production. Had you hand-written the camelCase interface instead, both edits would compile silently and the new field would simply never arrive. ## The wire type is also an assumption The second gap is upstream of the rename. `JSON.parse` is declared to return `any`, and `Response.json()` resolves to a value with no useful type either; whatever you annotate the parsed result with is a claim you have not verified. So even a correct mapper only converts *if the input was what you said it was*. At a trust boundary — a network response, a message queue, a file — you want an actual validation step that inspects the value and either produces the typed shape or fails loudly. Then the chain is honest: validated wire object → mapper → domain object, with the remapped type describing the last arrow. ## Should you rename at all? Worth saying out loud in an interview, because the strongest answer questions the premise. Renaming buys internal consistency and costs you a translation layer plus a set of names that do not match the API docs, the network tab, or the backend team's vocabulary. Two defensible positions: keep the wire names end to end and accept snake_case in your domain code, or rename exactly once at the boundary and never let a wire-shaped object past it. The failure is the third option — renaming *sometimes*, by assertion, in whichever module needed it — which produces two vocabularies for the same field and no single place where the translation is checked. ## Related erasure traps in the same area The same reasoning covers the filtering form. A type that drops function-valued keys does not remove methods from an object; if you serialise that object, the methods were never going to appear anyway, but if you `Object.keys` it, the filtered keys are all still there. And a type that renames keys does not change what a spread copies, what a `for` loop visits, or what a serialiser writes. Whenever a mapped type appears to "do" something, ask which emitted JavaScript would be doing it — the answer is always none. ## What a strong answer sounds like State the erasure rule first, in one sentence. Show the assertion trap concretely, because that is the mistake being probed. Then give the repair as a pair: a mapper whose signature is derived from the wire type so the compiler checks the translation, and validation at the boundary so the wire type is not merely wishful. Finish with the judgment call — renaming once at a boundary, or not at all — and you have covered the mechanism, the failure, and the design in the space of an interview answer.

  • What breaks if you keep the runtime mapper but hand-declare the camelCase type instead of deriving it?
    You lose the cross-check. Adding or renaming a field in the wire type no longer makes the mapper fail to compile, so the two shapes drift and the new field silently never reaches your domain object. Deriving the target from the source is what turns an upstream change into a build error.
  • Is annotating the parsed response with the wire type itself a guarantee?
    No. `JSON.parse` is declared to return `any`, so annotating the result is an unchecked claim about data you did not produce. At a trust boundary you need a real validation step that inspects the value at runtime and rejects it on mismatch; only then does the mapper's input type mean anything.
  • Does a mapped type that filters out function-valued keys remove methods from the object at runtime?
    No — it is erased like every other type. The object still carries its methods, `Object.keys` still sees whatever was enumerable, and a spread still copies what it always copied. The filtered type only constrains what your code is allowed to reference.

saying these in an interview costs you the question

  • Uses a type assertion to 'convert' the payload's keys
  • Says the compiler generates the renaming code
  • Thinks a mapped type validates the response shape
  • Believes annotating a parsed response makes it safe
  • Confuses the mapped-type as clause with an as assertion

context