diff --git a/lib/jtd.ts b/lib/jtd.ts index 6a7439d46..9ab13479c 100644 --- a/lib/jtd.ts +++ b/lib/jtd.ts @@ -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" diff --git a/lib/types/jtd-schema.ts b/lib/types/jtd-schema.ts index aecf5b3b0..8ecd84f5e 100644 --- a/lib/types/jtd-schema.ts +++ b/lib/types/jtd-schema.ts @@ -149,3 +149,67 @@ export type JTDSchemaType = Record} } + +type JTDDataDef> = + | (// ref + S extends {ref: string} + ? JTDDataDef + : // 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[] + : // properties + S extends { + properties: Record + optionalProperties?: Record + additionalProperties?: boolean + } + ? {-readonly [K in keyof S["properties"]]-?: JTDDataDef} & + { + -readonly [K in keyof S["optionalProperties"]]+?: JTDDataDef< + S["optionalProperties"][K], + D + > + } + : S extends { + properties?: Record + optionalProperties: Record + additionalProperties?: boolean + } + ? {-readonly [K in keyof S["properties"]]-?: JTDDataDef} & + { + -readonly [K in keyof S["optionalProperties"]]+?: JTDDataDef< + S["optionalProperties"][K], + D + > + } + : // values + S extends {values: infer V} + ? Record> + : // discriminator + S extends {discriminator: infer M; mapping: Record} + ? [M] extends [string] + ? { + [K in keyof S["mapping"]]: JTDDataDef & {[KM in M]: K} + }[keyof S["mapping"]] + : never + : // empty + unknown) + | (S extends {nullable: true} ? null : never) + +export type JTDDataType = S extends {definitions: Record} + ? JTDDataDef + : JTDDataDef> diff --git a/spec/types/jtd-schema.spec.ts b/spec/types/jtd-schema.spec.ts index a6f3c3dc3..02935767e 100644 --- a/spec/types/jtd-schema.spec.ts +++ b/spec/types/jtd-schema.spec.ts @@ -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() @@ -19,6 +19,11 @@ interface B { type MyData = A | B +interface LinkedList { + val: number + next?: LinkedList +} + const mySchema: JTDSchemaType = { discriminator: "type", mapping: { @@ -325,3 +330,106 @@ describe("JTDSchemaType", () => { void [isNull, numNotNull] }) }) + +describe("JTDDataType typechecks", () => { + it("should typecheck number schemas", () => { + const numSchema = {type: "float64"} as const + const num: TypeEquality, number> = true + + void [num] + }) + + it("should typecheck string schemas", () => { + const strSchema = {type: "string"} as const + const str: TypeEquality, string> = true + + void [str] + }) + + it("should typecheck timestamp schemas", () => { + const timeSchema = {type: "timestamp"} as const + const time: TypeEquality, string | Date> = true + + void [time] + }) + + it("should typecheck enum schemas", () => { + const enumSchema = {enum: ["a", "b"]} as const + const enumerated: TypeEquality, "a" | "b"> = true + + void [enumerated] + }) + + it("should typecheck elements schemas", () => { + const elementsSchema = {elements: {type: "float64"}} as const + const elem: TypeEquality, number[]> = true + + void [elem] + }) + + it("should typecheck properties schemas", () => { + const bothPropsSchema = { + properties: {a: {type: "float64"}}, + optionalProperties: {b: {type: "string"}}, + } as const + const both: TypeEquality, {a: number; b?: string}> = true + + const reqPropsSchema = {properties: {a: {type: "float64"}}} as const + const req: TypeEquality, {a: number}> = true + + const optPropsSchema = {optionalProperties: {b: {type: "string"}}} as const + const opt: TypeEquality, {b?: string}> = true + + void [both, req, opt] + }) + + it("should typecheck values schemas", () => { + const valuesSchema = {values: {type: "float64"}} as const + const values: TypeEquality, Record> = 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, 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, 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, LinkedList> = true + + void [ref, list] + }) + + it("should typecheck empty schemas", () => { + const emptySchema = {metadata: {}} as const + const empty: TypeEquality, unknown> = true + + void [empty] + }) +})