Last active
April 28, 2021 20:31
-
-
Save webstrand/8319200e0dba446147da27876e7b34cf to your computer and use it in GitHub Desktop.
JSON and JSONable type for representing serializable and deserialized objects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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