diff --git a/index.d.ts b/index.d.ts index 350d23bc..826113c9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,6 @@ import Ajv, { Options as AjvOptions } from "ajv" +import {FromSchema, JSONSchema} from 'json-schema-to-ts' + declare namespace build { interface BaseSchema { /** @@ -189,13 +191,14 @@ interface StandaloneOption extends build.Options { declare function build(schema: build.AnySchema, options: DebugOption): { code: string, ajv: Ajv }; declare function build(schema: build.AnySchema, options: DeprecateDebugOption): { code: string, ajv: Ajv }; declare function build(schema: build.AnySchema, options: StandaloneOption): string; -declare function build(schema: build.AnySchema, options?: build.Options): (doc: any) => any; -declare function build(schema: build.StringSchema, options?: build.Options): (doc: string) => string; -declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): (doc: number) => string; -declare function build(schema: build.NullSchema, options?: build.Options): (doc: null) => "null"; -declare function build(schema: build.BooleanSchema, options?: build.Options): (doc: boolean) => string; -declare function build(schema: build.ArraySchema | build.TupleSchema, options?: build.Options): (doc: any[]) => string; -declare function build(schema: build.ObjectSchema, options?: build.Options): (doc: object) => string; -declare function build(schema: build.Schema, options?: build.Options): (doc: object | any[] | string | number | boolean | null) => string; +declare function build(schema: build.AnySchema, options?: build.Options): (doc: TDoc) => any; +declare function build(schema: build.StringSchema, options?: build.Options): (doc: TDoc) => string; +declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): (doc: TDoc) => string; +declare function build(schema: build.NullSchema, options?: build.Options): (doc: TDoc) => "null"; +declare function build(schema: build.BooleanSchema, options?: build.Options): (doc: boolean) => string; +declare function build(schema: build.ArraySchema | build.TupleSchema, options?: build.Options): (doc: TDoc) => string; +declare function build(schema: build.ObjectSchema, options?: build.Options): (doc: TDoc) => string; +declare function build(schema: build.Schema, options?: build.Options): (doc: TDoc) => string; +declare function build(schema: TSchema, options?: build.Options): = FromSchema>(doc: TDoc) => any; export = build; diff --git a/index.js b/index.js index 91a2977a..d372094b 100644 --- a/index.js +++ b/index.js @@ -906,17 +906,7 @@ function buildValue (location, input) { switch (type) { case 'string': { code += ` - ${statement}( - typeof ${input} === "string" || - ${input} === null || - ${input} instanceof RegExp || - ( - typeof ${input} === "object" && - typeof ${input}.toString === "function" && - ${input}.toString !== Object.prototype.toString && - !(${input} instanceof Date) - ) - ) + ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.prototype.hasOwnProperty.call(${input}, "toString"))) ${nestedResult} ` break diff --git a/package.json b/package.json index 57e8453a..e6cdc86f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "benchmark": "node ./benchmark/bench-cmp-lib.js", "lint:fix": "standard --fix", "test:lint": "standard", - "test:typescript": "tsc --project ./test/types/tsconfig.json", + "test:typescript": "tsc --project ./test/types/tsconfig.json && tsd", "test:unit": "tap -J test/*.test.js test/**/*.test.js", "test": "npm run test:lint && npm run test:unit && npm run test:typescript" }, @@ -45,6 +45,7 @@ "simple-git": "^3.7.1", "standard": "^17.0.0", "tap": "^16.0.1", + "tsd": "^0.22.0", "typescript": "^4.0.2", "webpack": "^5.40.0" }, @@ -53,6 +54,7 @@ "ajv": "^8.10.0", "ajv-formats": "^2.1.1", "fast-uri": "^2.1.0", + "json-schema-to-ts": "^2.5.5", "rfdc": "^1.2.0" }, "standard": { @@ -60,5 +62,8 @@ "schema-validator.js" ] }, - "runkitExampleFilename": "example.js" + "runkitExampleFilename": "example.js", + "tsd": { + "directory": "test/types" + } } diff --git a/test/types/schema-inference.test-d.ts b/test/types/schema-inference.test-d.ts new file mode 100644 index 00000000..34f9bc96 --- /dev/null +++ b/test/types/schema-inference.test-d.ts @@ -0,0 +1,82 @@ +import { expectError } from "tsd"; +import build from "../.."; + +// Schema with constant value +const schema = { + type: "object", + properties: { + foo: { + const: "bar", + }, + }, + additionalProperties: false, +} as const; +const stringify = build(schema); + +expectError(stringify({ foo: "baz" })); +expectError(stringify({ foo: 1 })); +expectError(stringify({ foo: null })); + +stringify({ foo: "bar" }); + +// Schema with property multiple types +const schema1 = { + type: "object", + properties: { + foo: { + type: ["string", "integer", "null"], + }, + }, +} as const; +const stringify1 = build(schema1); +expectError(stringify1({ foo: true })); +stringify1({ foo: "bar" }); +stringify1({ foo: "bar", anotherOne: null }); +stringify1({ foo: 1 }); +stringify1({ foo: null }); + +// Schema with nested properties +const schema2 = { + type: "object", + properties: { + foo: { + type: "object", + properties: { + bar: { type: "object", properties: { baz: { type: "string" } } }, + }, + required: ["bar"], + }, + }, +} as const; +const stringify2 = build(schema2); +expectError( + stringify2({ + foo: { + bar: { baz: 1 }, + }, + }) +); +expectError( + stringify2({ + foo: { + bar: null, + }, + }) +); +stringify2({ foo: { bar: { baz: "baz" } } }); +stringify2({ foo: { bar: {} } }); + +// With inference +interface Schema { + id: string; + a?: number; +} + +const stringify3 = build({ + type: "object", + properties: { a: { type: "string" } }, +}); +stringify3({ id: "123" }); +stringify3({ a: 123, id: "123" }); +expectError(stringify3({ anotherOne: "bar" })); +expectError(stringify3({ a: "bar" })); diff --git a/test/typesArray.test.js b/test/typesArray.test.js index c6631a03..fa840fdf 100644 --- a/test/typesArray.test.js +++ b/test/typesArray.test.js @@ -438,35 +438,6 @@ test('object that is simultaneously a string and a json switched', (t) => { t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}') }) -test('class instance that is simultaneously a string and a json', (t) => { - t.plan(2) - - const schema = { - type: 'object', - properties: { - simultaneously: { - type: ['string', 'object'], - properties: { - foo: { type: 'string' } - } - } - } - } - - class Test { - toString () { return 'hello' } - } - - const likeObjectId = new Test() - - const stringify = build(schema) - const valueStr = stringify({ simultaneously: likeObjectId }) - t.equal(valueStr, '{"simultaneously":"hello"}') - - const valueObj = stringify({ simultaneously: { foo: likeObjectId } }) - t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}') -}) - test('should throw an error when type is array and object is null', (t) => { t.plan(1) const schema = {