Skip to content

Instantly share code, notes, and snippets.

@webstrand
Last active April 28, 2021 20:31
Show Gist options
  • Save webstrand/8319200e0dba446147da27876e7b34cf to your computer and use it in GitHub Desktop.
Save webstrand/8319200e0dba446147da27876e7b34cf to your computer and use it in GitHub Desktop.
JSON and JSONable type for representing serializable and deserialized objects
import { Assignable, Exact } from "./check"; // https://gist.github.com/webstrand/b0f79ef6ed37839d1432466fe8ddbc1a
export type Json = JsonPrimitive | JsonMap | JsonList;
export type JsonProperty = undefined | JsonPrimitive | JsonMap | JsonList;
export type JsonPrimitive = null | string | number | boolean;
export type JsonMap = { [key: string]: JsonProperty };
export type JsonList = Json[];
export type ReadonlyJson = ReadonlyJsonPrimitive | ReadonlyJsonMap | ReadonlyJsonList;
export type ReadonlyJsonProperty = undefined | ReadonlyJsonPrimitive | ReadonlyJsonMap | ReadonlyJsonList;
export type ReadonlyJsonPrimitive = null | string | number | boolean;
export type ReadonlyJsonMap = { readonly [key: string]: ReadonlyJsonProperty };
export type ReadonlyJsonList = readonly ReadonlyJson[];
// Primitives
Assignable<null, ReadonlyJson>(true);
Assignable<string, ReadonlyJson>(true);
Assignable<number, ReadonlyJson>(true);
Assignable<boolean, ReadonlyJson>(true);
Assignable<null | string | number | boolean, ReadonlyJson>(true);
Assignable<bigint, ReadonlyJson>(false);
Assignable<symbol, ReadonlyJson>(false);
Assignable<() => void, ReadonlyJson>(false);
Assignable<undefined, ReadonlyJson>(false);
Assignable<null | string | number | boolean | bigint, ReadonlyJson>(false);
// Maps
Assignable<{}, ReadonlyJson>(true);
Assignable<{ readonly [key: string]: null }, ReadonlyJson>(true);
Assignable<{ readonly [key: number]: string | undefined }, ReadonlyJson>(true);
Assignable<{ readonly foo: string | undefined, readonly 5: null }, ReadonlyJson>(true);
Assignable<{ readonly foo: [string, number, null] }, ReadonlyJson>(true);
Assignable<{ readonly [key: number]: bigint }, ReadonlyJson>(false);
Assignable<{ readonly foo: string | undefined, readonly 5: null | symbol }, ReadonlyJson>(false);
Assignable<{ readonly foo: [string, number, symbol] }, ReadonlyJson>(false);
// Properties
Assignable<ReadonlyJson | undefined, ReadonlyJsonProperty>(true);
Assignable<Exclude<ReadonlyJsonProperty, undefined>, ReadonlyJson>(true);
// Lists
Assignable<readonly [], ReadonlyJson>(true);
Assignable<readonly string[], ReadonlyJson>(true);
Assignable<readonly number[], ReadonlyJson>(true);
Assignable<readonly (string | number)[], ReadonlyJson>(true);
Assignable<readonly [null, string, number, boolean], ReadonlyJson>(true);
Assignable<readonly [{ readonly [key: string]: string | undefined }], ReadonlyJson>(true);
Assignable<readonly (string | number | symbol)[], ReadonlyJson>(false);
Assignable<readonly (string | number | undefined)[], ReadonlyJson>(false);
Assignable<readonly [null, string, number, boolean, undefined], ReadonlyJson>(false);
Assignable<readonly [{ readonly [key: string]: bigint }], ReadonlyJson>(false);
export type Jsonable = JsonablePrimitive | JsonableMap | JsonableList;
export type JsonableProperty = undefined | JsonablePrimitive | JsonableMap | JsonableList | { toJSON(propertyKey?: PropertyKey): JsonableProperty };
export type JsonablePrimitive = null | string | number | boolean | { toJSON(propertyKey?: PropertyKey): JsonablePrimitive }
export type JsonableMap = { readonly [key: string]: JsonableProperty } | { toJSON(propertyKey?: PropertyKey): Jsonable };
export type JsonableList = readonly Jsonable[] | { toJSON(propertyKey?: PropertyKey): JsonableList };
// All Json is Jsonable
Assignable<ReadonlyJson, Jsonable>(true);
Assignable<{ toJSON(): ReadonlyJson }, Jsonable>(true);
// toJSON can be union-typed
Assignable<{ toJSON(): "foo" | ["foo"] | { foo: string } | { toJSON(): "foo" | ["foo"] | { foo: string } } }, Jsonable>(true);
// Primitives
Assignable<null | { toJSON(): null }, Jsonable>(true);
Assignable<string | { toJSON(): string }, Jsonable>(true);
Assignable<number | { toJSON(): number }, Jsonable>(true);
Assignable<boolean | { toJSON(): boolean }, Jsonable>(true);
Assignable<null | string | number | boolean | { toJSON(): null | string | number | boolean }, Jsonable>(true);
Assignable<{ toJSON(): { toJSON(): { toJSON(): string } } }, Jsonable>(true);
Assignable<bigint, Jsonable>(false);
Assignable<{ toJSON(): bigint }, Jsonable>(false);
Assignable<symbol, Jsonable>(false);
Assignable<{ toJSON(): symbol }, Jsonable>(false);
Assignable<() => void, Jsonable>(false);
Assignable<{ toJSON(): () => void }, Jsonable>(false);
Assignable<undefined, Jsonable>(false);
Assignable<{ toJSON(): undefined }, Jsonable>(false);
Assignable<null | string | number | boolean | bigint, Jsonable>(false);
Assignable<null | string | number | boolean | { toJSON(): bigint }, Jsonable>(false);
Assignable<{ toJSON(): { toJSON(): { toJSON(): symbol } } }, Jsonable>(false);
// Maps
Assignable<{} | { toJSON(): {} }, Jsonable>(true);
Assignable<{ readonly [key: string]: null } | { toJSON(): { readonly [key: string]: null } }, Jsonable>(true);
Assignable<{ readonly [key: number]: string | undefined } | { toJSON(): { readonly [key: number]: string | undefined } }, Jsonable>(true);
Assignable<{ readonly foo: string | undefined, readonly 5: null } | { toJSON(): { readonly foo: string | undefined, readonly 5: null } }, Jsonable>(true);
Assignable<{ readonly foo: [string, number, null] } | { toJSON(): { readonly foo: [string, number, null] } }, Jsonable>(true);
Assignable<{ readonly foo: { toJSON(): { readonly foo: { toJSON(): { toJSON(): [] } } } } }, Jsonable>(true);
Assignable<{ readonly [key: number]: bigint }, Jsonable>(false);
Assignable<{ toJSON(): { readonly [key: number]: bigint } }, Jsonable>(false);
Assignable<{ readonly foo: string | undefined, readonly 5: null | symbol }, Jsonable>(false);
Assignable<{ toJSON(): { readonly foo: string | undefined, readonly 5: null | symbol } }, Jsonable>(false);
Assignable<{ readonly foo: [string, number, symbol] }, Jsonable>(false);
Assignable<{ toJSON(): { readonly foo: [string, number, symbol] } }, Jsonable>(false);
// Properties
Assignable<Jsonable | undefined | { toJSON(): undefined }, JsonableProperty>(true);
// can't actually recursively remove toJSON, this is a poor check:
Assignable<Exclude<JsonableProperty, undefined | { toJSON(): unknown }>, Jsonable>(true);
// Lists
Assignable<readonly [] | { toJSON(): readonly [] }, Jsonable>(true);
Assignable<readonly string[] | { toJSON(): readonly string[] }, Jsonable>(true);
Assignable<readonly number[] | { toJSON(): readonly number[] }, Jsonable>(true);
Assignable<readonly (string | number)[] | { toJSON(): readonly (string | number)[] }, Jsonable>(true);
Assignable<readonly [null, string, number, boolean] | { toJSON(): readonly [null, string, number, boolean] }, Jsonable>(true);
Assignable<readonly [{ readonly [key: string]: string | undefined }] | { toJSON(): readonly [{ readonly [key: string]: string | undefined }] }, Jsonable>(true);
Assignable<readonly [{ toJSON(): readonly [{toJSON(): { toJSON(): {} } }] }], Jsonable>(true);
Assignable<readonly (string | number | symbol)[], Jsonable>(false);
Assignable<{ toJSON(): readonly (string | number | symbol)[] }, Jsonable>(false);
Assignable<readonly (string | number | undefined)[], Jsonable>(false);
Assignable<{ toJSON(): readonly (string | number | undefined)[] }, Jsonable>(false);
Assignable<readonly [null, string, number, boolean, undefined], Jsonable>(false);
Assignable<{ toJSON(): readonly [null, string, number, boolean, undefined] }, Jsonable>(false);
Assignable<readonly [{ readonly [key: string]: bigint }], Jsonable>(false);
Assignable<{ toJSON(): readonly [{ readonly [key: string]: bigint }] }, Jsonable>(false);
export type JsonableFromJson<J extends ReadonlyJson> = J | JsonablePrimitiveFromJson<J> | JsonableListFromJson<J> | JsonableMapFromJson<J> | { toJSON(propertyKey?: string | number): JsonableFromJson<J> };
type JsonableFromJsonProperty<J extends ReadonlyJsonProperty> = J | JsonablePrimitiveFromJson<J> | JsonableListFromJson<J> | JsonableMapFromJson<J> | { toJSON(propertyKey?: string | number): JsonableFromJsonProperty<J> };
type JsonableFromJsonPrimitive<J extends ReadonlyJsonPrimitive> = J | { toJSON(propertyKey?: string | number): JsonableFromJsonPrimitive<J> }
type JsonableFromJsonMap<J extends ReadonlyJsonMap> = J | { [P in keyof J]: JsonableFromJsonProperty<J[P]> } | { toJSON(propertyKey?: string | number): JsonableFromJsonMap<J> };
type JsonableFromJsonList<J extends ReadonlyJsonList> = J | { [P in keyof J]: J[P] extends ReadonlyJson ? JsonableFromJson<J[P]> : never } | { toJSON(propertyKey?: string | number): JsonableFromJsonList<J> };
// Needed for recursive type generation, we accept JsonProperty
// so that both FromJson and FromJsonProperty can use the same
// conditionals.
type JsonablePropertyFromJson<J extends ReadonlyJsonProperty> = JsonableFromJsonProperty<J>;
type JsonablePrimitiveFromJson<J extends ReadonlyJsonProperty> = J extends ReadonlyJsonPrimitive ? JsonableFromJsonPrimitive<J> : never;
type JsonableMapFromJson<J extends ReadonlyJsonProperty> = J extends ReadonlyJsonMap ? JsonableFromJsonMap<J> : never;
type JsonableListFromJson<J extends ReadonlyJsonProperty> = J extends ReadonlyJsonList ? JsonableFromJsonList<J> : never;
// toJSON can be union-typed
Assignable<{ toJSON(): "foo" | ["foo"] | { foo: string } | { toJSON(): "foo" | ["foo"] | { foo: string } } }, JsonableFromJson<"foo" | ["foo"] | { foo: string }>>(true);
// Primitives
Assignable<null | { toJSON(): null }, JsonableFromJson<null>>(true);
Assignable<string | { toJSON(): string }, JsonableFromJson<string>>(true);
Assignable<number | { toJSON(): number }, JsonableFromJson<number>>(true);
Assignable<boolean | { toJSON(): boolean }, JsonableFromJson<boolean>>(true);
Assignable<null | string | number | boolean | { toJSON(): null | string | number | boolean }, JsonableFromJson<null | string | number | boolean>>(true);
Assignable<{ toJSON(): { toJSON(): { toJSON(): string } } }, JsonableFromJson<string>>(true);
Assignable<null | { toJSON(): null }, JsonableFromJson<string>>(false);
Assignable<string | { toJSON(): string }, JsonableFromJson<number>>(false);
Assignable<null | string | number | boolean | { toJSON(): null | string | number | boolean }, JsonableFromJson<string | number | boolean>>(false);
Assignable<{ toJSON(): { toJSON(): { toJSON(): string } } }, JsonableFromJson<number>>(false);
// Maps
Assignable<{} | { toJSON(): {} }, JsonableFromJson<{}>>(true);
Assignable<{ readonly [key: string]: null } | { toJSON(): { readonly [key: string]: null } }, JsonableFromJson<{ readonly [key: string]: null }>>(true);
Assignable<{ readonly [key: number]: string | undefined } | { toJSON(): { readonly [key: number]: string | undefined } }, JsonableFromJson<{ readonly [key: number]: string | undefined }>>(true);
Assignable<{ readonly foo: string | undefined, readonly 5: null } | { toJSON(): { readonly foo: string | undefined, readonly 5: null } }, JsonableFromJson<{ readonly foo: string | undefined, readonly 5: null }>>(true);
Assignable<{ readonly foo: [string, number, null] } | { toJSON(): { readonly foo: [string, number, null] } }, JsonableFromJson<{ readonly foo: [string, number, null] }>>(true);
Assignable<{ readonly foo: { toJSON(): { readonly foo: { toJSON(): { toJSON(): [] } } } } }, JsonableFromJson<{ readonly foo: { readonly foo: [] }}>>(true);
Assignable<{ readonly [key: string]: null } | { toJSON(): { readonly [key: string]: string } }, JsonableFromJson<{ readonly [key: string]: string }>>(false);
Assignable<{}, JsonableFromJson<{ foo: string }>>(false);
// Lists
Assignable<readonly [] | { toJSON(): readonly [] }, JsonableFromJson<readonly []>>(true);
Assignable<readonly string[] | { toJSON(): readonly string[] }, JsonableFromJson<readonly string[]>>(true);
Assignable<readonly number[] | { toJSON(): readonly number[] }, JsonableFromJson<readonly number[]>>(true);
Assignable<readonly (string | number)[] | { toJSON(): readonly (string | number)[] }, JsonableFromJson<readonly (string | number)[]>>(true);
Assignable<readonly [null, string, number, boolean] | { toJSON(): readonly [null, string, number, boolean] }, JsonableFromJson<readonly [null, string, number, boolean]>>(true);
Assignable<readonly [{ readonly [key: string]: string | undefined }] | { toJSON(): readonly [{ readonly [key: string]: string | undefined }] }, JsonableFromJson<readonly [{ readonly [key: string]: string | undefined }]>>(true);
Assignable<readonly [{ toJSON(): readonly [{toJSON(): { toJSON(): {} } }] }], JsonableFromJson<readonly [readonly [{}]]>>(true);
Assignable<readonly [{ toJSON(): readonly [{toJSON(): { toJSON(): {} } }] }], JsonableFromJson<readonly [readonly [string]]>>(false);
Assignable<readonly (string | number)[] | { toJSON(): readonly (string | number | null)[] }, JsonableFromJson<readonly (string | number)[]>>(false);
type NonJson = symbol | undefined | ((...args: any) => any);
export type JsonFrom<T> =
T extends { toJSON(): infer U } ? JsonFrom<U>
: T extends null | string | number | boolean ? T
: T extends readonly unknown[] ? JsonFromArray<T>
: T extends { readonly [key: string]: unknown } ? JsonFromObject<T>
: never;
type JsonFromObject<T extends { readonly [key: string]: unknown }> = { -readonly [P in keyof T as T[P] extends NonJson ? never : P]: JsonPropertyFrom<T[P]> };
type JsonPropertyFrom<T> = T extends NonJson ? never : JsonFrom<T>;
type JsonFromArray<T extends readonly unknown[]> = { -readonly [P in keyof T]: JsonElementFrom<T[P]> };
type JsonElementFrom<T> = T extends NonJson ? null : JsonFrom<T>;
// Types that get stripped by default
Exact<JsonFrom<undefined>, never>(true);
Exact<JsonFrom<bigint>, never>(true);
Exact<JsonFrom<symbol>, never>(true);
Exact<JsonFrom<() => void>, never>(true);
// Primitives
Exact<JsonFrom<null | { toJSON(): null }>, null>(true);
Exact<JsonFrom<string | { toJSON(): string }>, string>(true);
Exact<JsonFrom<number | { toJSON(): number }>, number>(true);
Exact<JsonFrom<boolean | { toJSON(): boolean }>, boolean>(true);
Exact<JsonFrom<{ toJSON(): { toJSON(): string }}>, string>(true);
// Arrays
Exact<JsonFrom<null[] | { toJSON(): readonly null[] }>, null[]>(true);
Exact<JsonFrom<string[] | { toJSON(): readonly string[] }>, string[]>(true);
Exact<JsonFrom<number[] | { toJSON(): readonly number[] }>, number[]>(true);
Exact<JsonFrom<boolean[] | { toJSON(): readonly boolean[] }>, boolean[]>(true);
// Other
Exact<JsonFrom<Date>, string>(true);
export type JsonFromJsonable<T extends Jsonable> = JsonFrom<T>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment