How do you type a JSON value that nests objects and arrays to any depth?
Declare a single type alias Json describing any value JSON.parse can return.
Requirements: cover the primitives plus null; allow an array of JSON values; allow an object whose property values are JSON values. Nesting must work to any depth, without a hand-written depth limit and without falling back to any.
type Json = /* your code here */;
const ok: Json = { a: [1, 'x', { b: [null, true] }] };
Write the implementation.
A type alias may reference itself, so Json is a recursive union: type Json = string | number | boolean | null | Json[] | { [k: string]: Json }. Both self-references sit inside an array or an index signature, which defers the recursion, so the checker resolves it lazily and nesting works to any depth.
- ✗Believing a type alias may never refer to itself
- ✗Reaching for
anyorunknowninstead of expressing the recursion directly - ✗Threading a hand-rolled depth counter a lazily-resolved alias does not need
- →What changes if you declare
Jsonas aninterfaceinstead of a type alias? - →When does a recursive type hit the compiler's instantiation-depth limit?
A TypeScript type alias may reference itself as long as the reference sits in a deferred position — inside an array, a tuple, an index signature, or an object property. That is what lets one declaration describe arbitrarily nested JSON.
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
const ok: Json = { a: [1, 'x', { b: [null, true] }] };
// @ts-expect-error — a function is not a JSON value
const bad: Json = { a: () => 1 };
The checker does not eagerly expand Json to some fixed depth: it substitutes the definition only while checking a concrete value, and only as deep as that value actually goes. That is why no depth counter is required.