Typorithm · hard

3. JSON DTOs, omission, null and numeric limits

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

3. JSON DTOs, omission, null and numeric limits

0%

Concept and contract

Design wire contracts whose serialized meaning matches the application’s intent.

A wire DTO should describe JSON-compatible data, not every in-memory object. Dates serialize as strings through their JSON behavior, undefined object properties are omitted, and bigint is not supported by ordinary JSON.stringify without a custom policy. Type annotations do not change those runtime rules.

Use explicit string timestamps and string identifiers when the wire contract requires them. Large integer identifiers can lose precision as JavaScript numbers. Keep transport representation separate from richer internal objects such as Date, Map or class instances.

Mechanism and worked example

The example constructs a patch where an absent website means leave unchanged and null means explicitly clear.

The example constructs a patch where an absent website means leave unchanged and null means explicitly clear. A string means replace. exactOptionalPropertyTypes helps distinguish omission from a present undefined value, but the endpoint still needs executable parsing and authorization.

JSON.stringify omits undefined object properties, while undefined array positions become null. This difference can change meaning if a developer treats serialization as a neutral copy operation. Document the endpoint’s patch semantics instead of relying on incidental serialization behavior.

Complete worked example

typescript

type WebsitePatch = { website?: string | null };
const keep: WebsitePatch = {};
const clear: WebsitePatch = { website: null };
const replace: WebsitePatch = { website: "https://example.com" };
Reasoning
The three constructions encode three different patch operations deliberately.

Counterexamples and common mistakes

The rejected patch writes undefined to an optional string-or-null property under exactOptionalPropertyTypes.

The rejected patch writes undefined to an optional string-or-null property under exactOptionalPropertyTypes. Omitting the property is the intended unchanged operation. Disabling the flag could allow the write statically without making the resulting JSON semantics less ambiguous.

Do not send an entire internal entity as a DTO merely because its annotation is narrower. Explicitly project allowed fields, and do not confuse a patch contract with permission to edit every resource. Sensitive fields and account ownership require separate server policy.

Rejected counterexample: expected compiler diagnostic

typescript

type WebsitePatch = { website?: string | null };
// @ts-expect-error Omit the property instead of writing undefined.
const invalid: WebsitePatch = { website: undefined };
Reasoning
A present undefined is not the same contract as an omitted optional field.

Independent practice

Implement websitePatch from a three-state command: keep, clear or replace with text.

Implement websitePatch from a three-state command: keep, clear or replace with text. Return an object whose serialized output is respectively {}, {website:null}, or {website:theValue}. Start with those three concrete expected objects before writing the union.

Test JSON output, not only property reads, because omission is part of the contract. Optional extension: validate replacement URLs. Keep URL policy separate from the basic transport-semantics exercise so the key distinction remains clear.

Step 1: Write the keep and clear JSON shapes. Step 2: Add the replacement branch.

Practice starter: solve before reading the solution

typescript

type Command = { kind: "keep" } | { kind: "clear" } | { kind: "replace"; value: string };
function websitePatch(command: Command): { website?: string | null } { throw new Error("TODO"); }
Reasoning
Write the three expected JSON objects first, then implement their command mapping.
  • Hint 1: Omission means unchanged under this contract.
  • Hint 2: Null means explicit clearing.

Solution, tests and trade-offs

The solution maps each command to an explicit object construction.

The solution maps each command to an explicit object construction. The keep branch creates no website property; the clear branch uses null deliberately. This is more reliable than assigning undefined and hoping the serializer communicates the desired patch operation.

Assert both Object.hasOwn and JSON.stringify for the keep case. Compile the rejected undefined assignment. A later refactor that uses one broad object spread should retain these tests so omission and clearing cannot silently collapse into the same operation.

Practice solution and acceptance checks

typescript

type Command = { kind: "keep" } | { kind: "clear" } | { kind: "replace"; value: string };
function websitePatch(command: Command): { website?: string | null } {
  switch(command.kind) { case "keep": return {}; case "clear": return {website:null}; case "replace": return {website:command.value}; }
}

// Acceptance checks
if(Object.hasOwn(websitePatch({kind:"keep"}),"website")||JSON.stringify(websitePatch({kind:"clear"}))!==JSON.stringify({website:null})||websitePatch({kind:"replace",value:"x"}).website!=="x")throw new Error("patch");
Reasoning
Explicit construction preserves omission, clearing and replacement as separate operations.

Interview reasoning and next step

For interviews, explain absent versus undefined versus null in object reads and JSON output.

For interviews, explain absent versus undefined versus null in object reads and JSON output. Describe why a timestamp string or large id string may be a better DTO representation than an internal Date or number.

Continue with asynchronous execution. Correct wire data still needs cancellation, ordering and explicit failure handling before it can safely drive a changing interface.

Optional stretch — after the core task works: Validate replacement URLs independently.

Design wire contracts whose serialized meaning matches the application’s intent.