Recursive: an array's elements and an object's property values are
themselves JsonValues, so a nested payload is checked all the way down
rather than only at its top level.
The object arm admits undefined deliberately.JSON.stringify drops a
property whose value is undefined rather than failing, so
{ note: string | undefined } — the shape an optional field takes once it is
written out — is serializable and is accepted here. What the type rejects is
the set JSON.stringify cannot represent: bigint (which throws), plus
functions and symbols (which it silently drops, losing data the caller
believed it was sending).
Three limits are worth knowing before you reach them, and none is
expressible in a type. A circular structure throws at runtime; no type can
express acyclicity. A named interface is not assignable, because TypeScript
grants implicit index signatures only to object-literal types — declare the
payload with a type alias, or extend Record<string, JsonValue | undefined>. And NaN, Infinity and -Infinity are members of number
that JSON has no representation for: JSON.stringify normalizes each of them
to null rather than failing, so the value is silently changed rather than
refused. TypeScript cannot exclude them from number, and a runtime check
would mean walking every payload on the hot path — send the number as a
string when the distinction matters.
A value
JSON.stringifycan serialize.Recursive: an array's elements and an object's property values are themselves
JsonValues, so a nested payload is checked all the way down rather than only at its top level.The object arm admits
undefineddeliberately.JSON.stringifydrops a property whose value isundefinedrather than failing, so{ note: string | undefined }— the shape an optional field takes once it is written out — is serializable and is accepted here. What the type rejects is the setJSON.stringifycannot represent:bigint(which throws), plus functions and symbols (which it silently drops, losing data the caller believed it was sending).Three limits are worth knowing before you reach them, and none is expressible in a type. A circular structure throws at runtime; no type can express acyclicity. A named
interfaceis not assignable, because TypeScript grants implicit index signatures only to object-literal types — declare the payload with atypealias, or extendRecord<string, JsonValue | undefined>. AndNaN,Infinityand-Infinityare members ofnumberthat JSON has no representation for:JSON.stringifynormalizes each of them tonullrather than failing, so the value is silently changed rather than refused. TypeScript cannot exclude them fromnumber, and a runtime check would mean walking every payload on the hot path — send the number as a string when the distinction matters.