Typorithm · hard

1. Runtime decoding at an API boundary

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

1. Runtime decoding at an API boundary

0%

Concept and contract

Convert unknown JSON into a small trusted DTO with explicit failure behavior.

A typed API starts at the boundary where external bytes become values. response.json() does not prove the response matches an interface, and a generic fetch<T> cannot manufacture that proof. Receive uncertain data as unknown and run an executable decoder before exposing a trusted contract.

Keep the exercise small: one role with id and title. A hand-written decoder makes every check visible. A schema library can provide the same boundary in a larger application, but its schema, error policy and runtime dependency still need deliberate design.

Mechanism and worked example

The example checks object presence and each required primitive field before constructing a fresh Role.

The example checks object presence and each required primitive field before constructing a fresh Role. That projection also discards unrelated fields. A nonempty-id rule is a separate business constraint and is enforced explicitly rather than assumed from the string annotation.

Return a tagged parse result when callers need recoverable validation failures. Throwing is also possible if the surrounding API convention expects it. Either way, preserve a stable failure category and avoid logging entire payloads that may contain personal data.

Complete worked example

typescript

type Role = { id: string; title: string };
function decodeRole(raw: unknown): Role | undefined {
  if(typeof raw !== "object" || raw === null || !("id" in raw) || typeof raw.id !== "string" || !raw.id
    || !("title" in raw) || typeof raw.title !== "string") return undefined;
  return {id:raw.id,title:raw.title};
}
Reasoning
Only the fresh projected result is trusted after all required field checks pass.

Counterexamples and common mistakes

The rejected assignment moves unknown directly into Role.

The rejected assignment moves unknown directly into Role. A type assertion would silence the compiler without inspecting a single field. This can create a misleading chain where every downstream function appears typed but the original boundary never established its assumptions.

Do not coerce arbitrary objects through String(value) or default missing ids to empty strings unless the API contract requires that behavior. Silent repair can hide provider regressions and create duplicate identities or unusable records.

Rejected counterexample: expected compiler diagnostic

typescript

type Role = { id: string; title: string };
const raw: unknown = {};
// @ts-expect-error The external value has not been decoded.
const role: Role = raw;
Reasoning
An interface cannot establish the shape of an unknown external value.

Independent practice

Implement decodeCompany with id, name and optional website.

Implement decodeCompany with id, name and optional website. Require a nonempty id and name. If website exists, require a string; omit it from the projected output when absent. Begin with id validation, then add one field at a time.

Test null, an empty name, a numeric website and a valid record without website. Optional extension: validate the website as an allowed URL. Keep that separate from the basic field decoder so the task remains approachable.

Step 1: Validate id and name first. Step 2: Handle absent versus present website explicitly.

Practice starter: solve before reading the solution

typescript

type Company = { id: string; name: string; website?: string };
function decodeCompany(raw: unknown): Company | undefined { throw new Error("TODO"); }
Reasoning
Add and test one field check at a time, preserving absent optional properties.
  • Hint 1: Do not coerce malformed fields into successful DTOs.
  • Hint 2: Construct a fresh public result.

Solution, tests and trade-offs

The solution checks each field and conditionally constructs the optional property under exactOptionalPropertyTypes.

The solution checks each field and conditionally constructs the optional property under exactOptionalPropertyTypes. It rejects a present undefined website because this exercise accepts only absent or string. That policy should match the real wire format rather than an accidental compiler default.

Run both malformed and valid fixtures. A static receiving assignment confirms the returned contract, while runtime assertions confirm the decoder rejects bad input and strips unrelated fields. Neither replaces an integration fixture from the actual provider.

Practice solution and acceptance checks

typescript

type Company = { id: string; name: string; website?: string };
function decodeCompany(raw: unknown): Company | undefined {
  if(typeof raw !== "object" || raw === null || !("id" in raw) || typeof raw.id !== "string" || !raw.id.trim()
    || !("name" in raw) || typeof raw.name !== "string" || !raw.name.trim()) return undefined;
  if("website" in raw && typeof raw.website !== "string") return undefined;
  return {id:raw.id,name:raw.name,...("website" in raw ? {website:raw.website as string} : {})};
}

// Acceptance checks
if(decodeCompany({id:"c",name:"Acme"})?.name!=="Acme")throw new Error("valid");for(const x of [null,{id:"c",name:""},{id:"c",name:"Acme",website:2}])if(decodeCompany(x)!==undefined)throw new Error("invalid");
Reasoning
The projection follows the declared optional-field policy and strips unrelated input fields.

Interview reasoning and next step

In interviews, trace unknown input into a validated DTO and identify the exact point where trust is established.

In interviews, trace unknown input into a validated DTO and identify the exact point where trust is established. Explain why a schema library is useful without claiming that an interface alone validates JSON. Mention error reporting and private-data handling.

The next lesson connects this decoder to HTTP status handling and fetch. Keep transport errors separate from payload-shape errors so retries and user messages can respond appropriately.

Optional stretch — after the core task works: Add a documented URL policy.

Convert unknown JSON into a small trusted DTO with explicit failure behavior.