json
JSONValue, Jsonify, Jsonifiable — the JSON shape lattice.
import type {
JSONPrimitive, JSONObject, JSONArray, JSONValue,
Jsonify, Jsonifiable, IsJsonValue,
} from '@gentleduck/ttest/json'
The closed lattice of JSON-serializable values, plus two transforms that coerce arbitrary types into JSON-shaped types. Use Jsonify when the contract is "what comes out the other side of JSON.parse(JSON.stringify(...))" and Jsonifiable when the contract is "values I will accept as input to JSON.stringify".
JSONPrimitive
type JSONPrimitive = string | number | boolean | null
The leaf types in a JSON document. Notably excludes undefined, bigint, symbol, and Date.
JSONObject / JSONArray / JSONValue
type JSONObject = { [key: string]: JSONValue }
type JSONArray = JSONValue[]
type JSONValue = JSONPrimitive | JSONObject | JSONArray
A recursive lattice. JSONValue is the closure — every shape produced by JSON.parse satisfies it.
const ok: JSONValue = { items: [1, 'two', { nested: null }] }
const bad: JSONValue = { date: new Date() } // Error — Date not assignable
Jsonify
type Jsonify<T>
Recursively coerce T into a JSON-serializable shape. The rules:
- Functions are dropped (set to
never, which removes the key). Dateis dropped — even thoughJSON.stringifywould coerce it to a string, the runtime type and the JSON shape diverge, so the helper is conservative.- Keys literally named
passwordortoJSONare dropped. - Arrays recurse on their element type.
- Plain objects recurse on their values.
type User = {
id: number
name: string
password: string
verify(): void
created: Date
}
type J = Jsonify<User>
// ^? { id: number; name: string }
type Nested = Jsonify<{ items: { id: number; created: Date }[] }>
// ^? { items: { id: number }[] }
Edge cases:
Jsonify<Date>isnever. Coerce tostring(ISO date) at the boundary if you need it.Jsonify<undefined>isnever—undefinedis not part ofJSONPrimitive. Wrap optional fields explicitly:Jsonify<{ field: string | null }>.
Jsonifiable
type Jsonifiable<T = unknown>
A value that survives a JSON.stringify round-trip. Less destructive than Jsonify:
- Functions are dropped.
- Objects with
toJSON()are accepted; thetoJSONreturn type substitutes for the object. - Plain objects recurse on their values.
Dateis accepted (itstoJSONreturns a string).
type T = Jsonifiable<{ a: number; b: () => void }>
// ^? { a: number }
type D = Jsonifiable<Date>
// ^? string (via Date.prototype.toJSON)
function send<T>(payload: Jsonifiable<T>) { /* ... */ }
send({ id: 1, name: 'Ada' }) // ok
send({ id: 1, callback: () => {} }) // accepted — callback is dropped at the type level
Pick Jsonify when you model the output of a JSON round-trip and Jsonifiable when you model the input.
IsJsonValue
type IsJsonValue<T> = T extends JSONValue ? true : false
true iff T is structurally assignable to JSONValue.
import type { AssertTrue, AssertFalse } from '@gentleduck/ttest/assert'
type _ = [
AssertTrue <IsJsonValue<{ a: 1; b: [2, 'three'] }>, 'plain json shape'>,
AssertFalse<IsJsonValue<{ d: Date }>, 'Date not in lattice'>,
AssertFalse<IsJsonValue<{ f: () => void }>, 'functions not in lattice'>,
]
Edge case: IsJsonValue<any> is boolean, not true — any distributes through both branches. Guard with IsAny if you suspect leaked any.