Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] JTDDataType #1458

Merged
merged 4 commits into from
Mar 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/jtd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export {KeywordCxt}
export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen"

import type {AnySchemaObject, SchemaObject, JTDParser} from "./types"
import type {JTDSchemaType} from "./types/jtd-schema"
export {JTDSchemaType}
import type {JTDSchemaType, JTDDataType} from "./types/jtd-schema"
export {JTDSchemaType, JTDDataType}
import AjvCore, {CurrentOptions} from "./core"
import jtdVocabulary from "./vocabularies/jtd"
import jtdMetaSchema from "./refs/jtd-schema"
Expand Down
66 changes: 66 additions & 0 deletions lib/types/jtd-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,69 @@ export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string,
// TODO these should only be allowed at the top level
definitions?: {[K in keyof D]: JTDSchemaType<D[K], D>}
}

type JTDDataDef<S, D extends Record<string, unknown>> =
| (// ref
S extends {ref: string}
? JTDDataDef<D[S["ref"]], D>
: // type
S extends {type: NumberType}
? number
: S extends {type: "string"}
? string
: S extends {type: "timestamp"}
? string | Date
: // enum
S extends {enum: readonly (infer E)[]}
? string extends E
? never
: [E] extends [string]
? E
: never
: // elements
S extends {elements: infer E}
? JTDDataDef<E, D>[]
: // properties
S extends {
properties: Record<string, unknown>
optionalProperties?: Record<string, unknown>
additionalProperties?: boolean
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
}
? {-readonly [K in keyof S["properties"]]-?: JTDDataDef<S["properties"][K], D>} &
{
-readonly [K in keyof S["optionalProperties"]]+?: JTDDataDef<
S["optionalProperties"][K],
D
>
} &
([S["additionalProperties"]] extends [true] ? Record<string, unknown> : unknown)
: S extends {
properties?: Record<string, unknown>
optionalProperties: Record<string, unknown>
additionalProperties?: boolean
}
? {-readonly [K in keyof S["properties"]]-?: JTDDataDef<S["properties"][K], D>} &
{
-readonly [K in keyof S["optionalProperties"]]+?: JTDDataDef<
S["optionalProperties"][K],
D
>
} &
([S["additionalProperties"]] extends [true] ? Record<string, unknown> : unknown)
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
: // values
S extends {values: infer V}
? Record<string, JTDDataDef<V, D>>
: // discriminator
S extends {discriminator: infer M; mapping: Record<string, unknown>}
? [M] extends [string]
? {
[K in keyof S["mapping"]]: JTDDataDef<S["mapping"][K], D> & {[KM in M]: K}
}[keyof S["mapping"]]
: never
: // empty
unknown)
| (S extends {nullable: true} ? null : never)

export type JTDDataType<S> = S extends {definitions: Record<string, unknown>}
? JTDDataDef<S, S["definitions"]>
: JTDDataDef<S, Record<string, never>>
133 changes: 132 additions & 1 deletion spec/types/jtd-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-interface,no-void */
import _Ajv from "../ajv_jtd"
import type {JTDSchemaType} from "../../dist/jtd"
import type {JTDSchemaType, JTDDataType} from "../../dist/jtd"
import chai from "../chai"
const should = chai.should()

Expand All @@ -19,6 +19,11 @@ interface B {

type MyData = A | B

interface LinkedList {
val: number
next?: LinkedList
}

const mySchema: JTDSchemaType<MyData> = {
discriminator: "type",
mapping: {
Expand Down Expand Up @@ -315,3 +320,129 @@ describe("JTDSchemaType", () => {
void [meta, emptyMeta, unknownMeta]
})
})

describe("JTDDataType", () => {
it("should typecheck number schemas", () => {
const numSchema = {type: "float64"} as const
const num: TypeEquality<JTDDataType<typeof numSchema>, number> = true

void [num]
})

it("should typecheck string schemas", () => {
const strSchema = {type: "string"} as const
const str: TypeEquality<JTDDataType<typeof strSchema>, string> = true

void [str]
})

it("should typecheck timestamp schemas", () => {
const timeSchema = {type: "timestamp"} as const
const time: TypeEquality<JTDDataType<typeof timeSchema>, string | Date> = true

void [time]
})

it("should typecheck enum schemas", () => {
const enumSchema = {enum: ["a", "b"]} as const
const enumerated: TypeEquality<JTDDataType<typeof enumSchema>, "a" | "b"> = true

// if you forget const on an enum it will error
const enumStringSchema = {enum: ["a", "b"]}
const enumString: TypeEquality<JTDDataType<typeof enumStringSchema>, never> = true
// also if not a string
const enumNumSchema = {enum: [3]} as const
const enumNum: TypeEquality<JTDDataType<typeof enumNumSchema>, never> = true

void [enumerated, enumString, enumNum]
})

it("should typecheck elements schemas", () => {
const elementsSchema = {elements: {type: "float64"}} as const
const elem: TypeEquality<JTDDataType<typeof elementsSchema>, number[]> = true

void [elem]
})

it("should typecheck properties schemas", () => {
const bothPropsSchema = {
properties: {a: {type: "float64"}},
optionalProperties: {b: {type: "string"}},
} as const
const both: TypeEquality<JTDDataType<typeof bothPropsSchema>, {a: number; b?: string}> = true

const reqPropsSchema = {properties: {a: {type: "float64"}}} as const
const req: TypeEquality<JTDDataType<typeof reqPropsSchema>, {a: number}> = true

const optPropsSchema = {optionalProperties: {b: {type: "string"}}} as const
const opt: TypeEquality<JTDDataType<typeof optPropsSchema>, {b?: string}> = true

const noAddSchema = {
optionalProperties: {b: {type: "string"}},
additionalProperties: false,
} as const
const noAdd: TypeEquality<JTDDataType<typeof noAddSchema>, {b?: string}> = true

const addSchema = {
optionalProperties: {b: {type: "string"}},
additionalProperties: true,
} as const
const add: TypeEquality<
JTDDataType<typeof addSchema>,
{b?: string; [key: string]: unknown}
> = true
const addVal: JTDDataType<typeof addSchema> = {b: "b", additional: 6}

void [both, req, opt, noAdd, add, addVal]
})

it("should typecheck values schemas", () => {
const valuesSchema = {values: {type: "float64"}} as const
const values: TypeEquality<JTDDataType<typeof valuesSchema>, Record<string, number>> = true

void [values]
})

it("should typecheck discriminator schemas", () => {
const discriminatorSchema = {
discriminator: "type",
mapping: {
a: {properties: {a: {type: "float64"}}},
b: {optionalProperties: {b: {type: "string"}}},
},
} as const
const disc: TypeEquality<JTDDataType<typeof discriminatorSchema>, A | B> = true

void [disc]
})

it("should typecheck ref schemas", () => {
const refSchema = {
definitions: {num: {type: "float64", nullable: true}},
ref: "num",
nullable: true,
} as const
const ref: TypeEquality<JTDDataType<typeof refSchema>, number | null> = true

// works for recursive schemas
const llSchema = {
definitions: {
node: {
properties: {val: {type: "float64"}},
optionalProperties: {next: {ref: "node"}},
},
},
ref: "node",
} as const
const list: TypeEquality<JTDDataType<typeof llSchema>, LinkedList> = true

void [ref, list]
})

it("should typecheck empty schemas", () => {
const emptySchema = {metadata: {}} as const
const empty: TypeEquality<JTDDataType<typeof emptySchema>, unknown> = true

void [empty]
})
})