JSON
The JSON types name what JSON.parse can give back and what JSON.stringify keeps. Use them as the
constraint on data that crosses a JSON boundary: a request or response body, a config file, a message
on a queue.
For the other named sets of types, such as PrimitiveTypes, see Type Sets.
JsonTypes, JsonPrimitive, JsonObject and JsonArray
Section titled “JsonTypes, JsonPrimitive, JsonObject and JsonArray”type JsonTypes = JsonPrimitive | JsonObject | JsonArraytype JsonPrimitive = boolean | number | string | nulltype JsonObject = { [key: string]: JsonTypes }type JsonArray = readonly JsonTypes[]🧰 type util — any value JSON.parse can produce.
import type { JsonTypes } from 'type-plus'
type R = { a: 1 } extends JsonTypes ? true : false // truetype R = { readonly a: readonly [1, 2] } extends JsonTypes ? true : false // truetype R = { a?: number } extends JsonTypes ? true : false // true
type R = Date extends JsonTypes ? true : false // falsetype R = { a: undefined } extends JsonTypes ? true : false // falseJsonTypes admits exactly what survives the round trip, so a Date, a Map, a function or an
undefined value is rejected where it is put in rather than silently lost on the way out.
undefinedis absent fromJsonPrimitive, because JSON has no such value.- A
JsonObjectkey may be absent but neverundefined.{ a?: number }is accepted, becauseJSON.stringifydrops a missing key and it comes back missing.{ a: undefined }is rejected. JsonArrayis readonly, so readonly arrays, tuples andas constdata are accepted.
Reading a key from a JsonObject is an index-signature read. Under noUncheckedIndexedAccess it is
JsonTypes | undefined, which is the honest type for parsed data, where no key is guaranteed to be there.
Without that option the read is JsonTypes. TypeScript has no index signature that rejects undefined on
the way in and adds it on the way out.
An interface is not assignable to JsonTypes, because TypeScript gives interfaces no implicit index
signature. Use JsonCompatible or IsJson for those.
JsonCompatible
Section titled “JsonCompatible”type JsonCompatible<T>🧰 type util — T with every part that is not JSON replaced by a message naming where it is.
Use it as a self-referencing constraint to accept a JSON-safe value of any declared type, including an
interface:
import type { JsonCompatible } from 'type-plus'
function save<T extends JsonCompatible<T>>(value: T) { ... }
interface User { id: number; tags: string[]; nick?: string }save(user) // oksave({ a: [1, 2] } as const) // ok
interface Post { author: { profile: { createdAt: Date } } }save(post)// Type 'Date' is not assignable to type '"not JSON at $.author.profile.createdAt"'save({ a: undefined })// Type 'undefined' is not assignable to type '"not JSON at $.a"'The path is a JSONPath from the root $. An array element shows as [*] and a tuple element by its index.
It rejects functions, objects with methods (such as Date and Map), bigint, symbol, and undefined
anywhere but the implicit value of an optional property.
IsJson and IsNotJson
Section titled “IsJson and IsNotJson”type IsJson<T, $O = {}>type IsNotJson<T, $O = {}>🎭 predicate — validate if T survives a JSON round trip, T being assignable to JsonCompatible<T>.
import type { IsJson } from 'type-plus'
type R = IsJson<User> // truetype R = IsJson<{ a: readonly [1, 2] }> // truetype R = IsJson<{ a: Date }> // falsetype R = IsJson<{ a: undefined }> // falsetype R = IsJson<string | Date> // boolean
type R = IsJson<string | Date, { selection: 'filter' }> // stringtype R = IsJson<string | Date, { distributive: false }> // falseThey take the selection, distributive and special-type branch options of the other predicates. See
Type branching. To find out where a type fails, constrain with
JsonCompatible: its error names the path.
Serialized and Deserialized
Section titled “Serialized and Deserialized”type Serialized<T extends JsonCompatible<T>, $O = {}>type Deserialized<S extends Serialized<any, { circular: boolean }>>🧰 type util — a string that carries the type of the value it was serialized from, and the type
that parsing it gives back.
JSON.parse returns any. So does flatted, which serializes
values with circular references: its declarations are parse(text: string): any and
stringify(value: any): string. Nothing ties the value you stringify to the value you parse:
import * as flatted from 'flatted'
const text = flatted.stringify(user) // stringconst back = flatted.parse(text) // anyback.nmae // no errorflatted.stringify({ createdAt: new Date() }) // no error, comes back as a stringJSON.parse(text) // no error, but the shape is wrongTwo lines of wrapper give flatted a type. Serialized<T, { circular: true }> tags the string with T
and with its format, and Deserialized reads T back:
import * as flatted from 'flatted'import type { Deserialized, JsonCompatible, Serialized } from 'type-plus'
const stringify = <T extends JsonCompatible<T>>(value: T) => flatted.stringify(value) as Serialized<T, { circular: true }>const parse = <S extends Serialized<any, { circular: true }>>(text: S) => flatted.parse(text) as Deserialized<S>
interface Person { name: string; friend?: Person }const ada: Person = { name: 'Ada' }ada.friend = ada
const text = stringify(ada) // Serialized<Person, { circular: true }>const back = parse(text) // Personback.friend === back // true: the cycle survivesNow the mistakes are compile errors:
stringify({ createdAt: new Date() })// Type 'Date' is not assignable to type '"not JSON at $.createdAt"'
parse('{"name":"Ada"}')// Argument of type 'string' is not assignable to parameter of type 'Serialized<any, { circular: true; }>'
JSON.parse(text) // still any: JSON.parse takes any stringThe same two lines work for JSON, with the default format:
const stringify = <T extends JsonCompatible<T>>(value: T) => JSON.stringify(value) as Serialized<T>const parse = <S extends Serialized<any>>(text: S) => JSON.parse(text) as Deserialized<S>
const user = parse(stringify({ id: 1, tags: ['a'] })) // { id: number; tags: string[] }The two formats do not assign to each other either way, so with both pairs of wrappers in a codebase, a
flatted string cannot reach the JSON parse, nor a JSON string the flatted one:
parse(flattedText)// Type '"Serialized<circular>"' is not assignable to type '"Serialized<json>"'.flattedParse(jsonText)// Type '"Serialized<json>"' is not assignable to type '"Serialized<circular>"'.At runtime the first gives back flatted’s internal array, and the second throws.
Serializedis a brandedstring(see Nominal Types). It is assignable tostring, so it can be sent or stored as one, but a plainstringis not assignable to it, nor is the serialized form of an unrelated type.IsStringgivestrueandIsStringLiteralgivesfalse.Tis constrained withJsonCompatible, so aninterface, a recursive type and a self-referencing interface are accepted, and the error names the path of the part that is not JSON.circularnames the format, not a property of the value. A type cannot prove a value is acyclic: the default,circular: false, means the string is plain JSON. A cyclic value passed toJSON.stringifystill throws at runtime.circular: booleanis either format.- The type is only as true as the cast in the wrapper. type-plus ships no runtime code for this, and no dependency on flatted.
Deprecated JSON* names
Section titled “Deprecated JSON* names”💀 deprecated since 8.0.0: the names before 8.0, kept as aliases until 9.0 — use JsonTypes,
JsonPrimitive, JsonObject and JsonArray. The new names title-case the acronym, as every type-plus
name does. See Migrating to v8.
Reference
Section titled “Reference”| Type | Description |
|---|---|
JsonTypes |
any value JSON.parse can produce |
JsonPrimitive |
boolean | number | string | null |
JsonObject |
string keys holding JsonTypes, never undefined |
JsonArray |
readonly JsonTypes[] |
JsonCompatible<T> |
T checked part by part; constrain with T extends JsonCompatible<T> |
IsJson<T, $O> |
whether T survives a JSON round trip |
IsNotJson<T, $O> |
whether T does not survive a JSON round trip |
Serialized<T, $O> |
a string carrying T; { circular: true } for a format such as flatted’s |
Deserialized<S> |
the type parsing a Serialized string gives back |
JSONTypes, JSONPrimitive, JSONObject, JSONArray |
💀 deprecated since 8.0.0, use the Json* names |
Source: src/json.ts,
src/json-compatible.ts,
src/is-json.ts,
src/is-not-json.ts,
src/serialized.ts and
src/deserialized.ts.