From 2ddde292231cd82f1d6805a78072ffbae807572b Mon Sep 17 00:00:00 2001 From: Dimava Date: Wed, 18 Sep 2024 21:13:58 +0300 Subject: [PATCH 01/11] feat: default factory --- ark/schema/roots/root.ts | 2 +- ark/schema/structure/optional.ts | 68 +++++++++++++++++++-- ark/schema/structure/prop.ts | 26 ++++++--- ark/type/__tests__/defaults.test.ts | 91 +++++++++++++++++++++++++++++ ark/type/methods/base.ts | 12 +++- 5 files changed, 184 insertions(+), 15 deletions(-) diff --git a/ark/schema/roots/root.ts b/ark/schema/roots/root.ts index 7f37a39da..4cc3fefbc 100644 --- a/ark/schema/roots/root.ts +++ b/ark/schema/roots/root.ts @@ -324,7 +324,7 @@ export abstract class BaseRoot< } default(value: unknown): this { - assertDefaultValueAssignability(this, value) + value = assertDefaultValueAssignability(this, value, null, true) return this.withMeta({ default: value }) } diff --git a/ark/schema/structure/optional.ts b/ark/schema/structure/optional.ts index 4fc7594c2..9d76a7b94 100644 --- a/ark/schema/structure/optional.ts +++ b/ark/schema/structure/optional.ts @@ -1,4 +1,4 @@ -import { printable, throwParseError, unset } from "@ark/util" +import { printable, throwParseError, unset, type Primitive } from "@ark/util" import type { BaseRoot } from "../roots/root.ts" import type { declareNode } from "../shared/declare.ts" import { ArkErrors } from "../shared/errors.ts" @@ -65,7 +65,8 @@ export class OptionalNode extends BaseProp<"optional"> { assertDefaultValueAssignability( this.value, this.inner.default, - this.serializedKey + this.serializedKey, + false ) } } @@ -95,14 +96,63 @@ export const Optional = { Node: OptionalNode } +const isPrimitive = (value: unknown): value is Primitive => + typeof value === "object" ? value === null : typeof value !== "function" +const isSimpleSerializeable = (value: unknown): (() => unknown) | false => { + if (value instanceof Date) return () => new Date(value) + if ( + Array.isArray(value) && + Object.getPrototypeOf(value) === Array.prototype && + value.every(isPrimitive) + ) + return () => value.slice() + if ( + typeof value === "object" && + value !== null && + Object.getPrototypeOf(value) === Object.prototype && + Object.getOwnPropertySymbols(value).length === 0 && + Object.getOwnPropertyNames(value).every(k => { + const prop = Object.getOwnPropertyDescriptor(value, k) + return ( + prop && + "value" in prop && + isPrimitive(prop.value) && + prop.writable === true && + prop.enumerable === true && + prop.configurable === true + ) + }) + ) + return () => ({ ...value }) + return false +} + export const assertDefaultValueAssignability = ( node: BaseRoot, value: unknown, - key = "" + key: string | null, + canOverrideValue: boolean ): unknown => { - const out = node.in(value) - if (out instanceof ArkErrors) - throwParseError(writeUnassignableDefaultValueMessage(out.message, key)) + if (!isPrimitive(value) && typeof value !== "function") { + if (!canOverrideValue) { + throwParseError( + writeNonPrimitiveNonFunctionDefaultValueMessage(key ?? "", value) + ) + } + const fn = isSimpleSerializeable(value) + if (!fn) { + throwParseError( + writeNonPrimitiveNonFunctionDefaultValueMessage(key ?? "", value) + ) + } + value = fn + } + const out = node.in(typeof value === "function" ? value() : value) + if (out instanceof ArkErrors) { + throwParseError( + writeUnassignableDefaultValueMessage(out.message, key ?? "") + ) + } return value } @@ -115,3 +165,9 @@ export type writeUnassignableDefaultValueMessage< baseDef extends string, defaultValue extends string > = `Default value ${defaultValue} is not assignable to ${baseDef}` + +export const writeNonPrimitiveNonFunctionDefaultValueMessage = ( + key: string, + value: unknown +): string => + `Default value${key && ` for key ${key}`} is not primitive so it should be constructor function (was ${printable(value)})` diff --git a/ark/schema/structure/prop.ts b/ark/schema/structure/prop.ts index 43d5f9ea1..693828e7c 100644 --- a/ark/schema/structure/prop.ts +++ b/ark/schema/structure/prop.ts @@ -124,16 +124,25 @@ export abstract class BaseProp< private premorphedDefaultValue: unknown = this.hasDefault() ? - this.value.includesMorph ? - this.value.assert(this.default) - : this.default + this.hasDefaultFactory() ? + () => + this.value.includesMorph ? + this.value.assert(this.default()) + : this.default() + : this.value.includesMorph ? this.value.assert(this.default) + : this.default : undefined private defaultValueMorphs: Morph[] = [ - data => { - data[this.key] = this.premorphedDefaultValue - return data - } + this.hasDefaultFactory() ? + data => { + data[this.key] = (this.premorphedDefaultValue as () => unknown)() + return data + } + : data => { + data[this.key] = this.premorphedDefaultValue + return data + } ] private defaultValueMorphsReference = registeredReference( @@ -143,6 +152,9 @@ export abstract class BaseProp< hasDefault(): this is Optional.Node & { default: unknown } { return "default" in this } + hasDefaultFactory(): this is Optional.Node & { default: () => unknown } { + return this.hasDefault() && typeof this.default === "function" + } traverseAllows: TraverseAllows = (data, ctx) => { if (this.key in data) { diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index 80409f96e..6ded98f57 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -364,4 +364,95 @@ contextualize(() => { ) }) }) + + describe("works with objects", () => { + it("default array in string", () => { + const t = type({ bar: type("number[] = []") }) + attest(t.assert({}).bar).snap([]) + attest(t.assert({}).bar !== t.assert({}).bar) + }) + it("default array", () => { + const t = type({ + foo: type("number[]").default([1]), + bar: type("number[]").default(() => [1]), + baz: type("number[]") + .pipe(v => v.map(e => e.toString())) + .default(() => [1]) + }) + const v1 = t.assert({}), + v2 = t.assert({}) + attest(v1).snap({ foo: [1], bar: [1], baz: ["1"] }) + attest(v1.foo !== v2.foo) + }) + it("default array is checked", () => { + attest(() => { + // @ts-expect-error + type({ foo: type("number[]").default(["a"]) }) + }).throws() + attest(() => { + // @ts-expect-error + type({ bar: type("number[]").default(() => ["a"]) }) + }).throws() + attest(() => { + // @ts-expect-error + type({ + baz: type("number[]") + .pipe(v => v.map(e => e.toString())) + .default(() => ["a"]) + }) + }).throws() + }) + it("disallows default array with non-primitive elements", () => { + ;[[], {}, new Date(), () => {}].forEach(v => { + attest(() => { + // @ts-expect-error + type({ foo: type("any[]").default(v) }) + }).throws() + }) + }) + it("default object", () => { + const t = type({ + foo: type({ "foo?": "string" }).default({}), + bar: type({ "foo?": "string" }).default(() => ({ foo: "foostr" })), + baz: type({ foo: "string = 'foostr'" }).default({}) + }) + const v1 = t.assert({}), + v2 = t.assert({}) + attest(v1).snap({ + foo: {}, + bar: { foo: "foostr" }, + baz: { foo: "foostr" } + }) + attest(v1.foo !== v2.foo) + }) + it("default object is checked", () => { + attest(() => { + // @ts-expect-error + type({ foo: type({ foo: "string" }).default({}) }) + }).throws() + attest(() => { + // @ts-expect-error + type({ + bar: type({ foo: "number" }).default(() => ({ foo: "foostr" })) + }) + }).throws() + }) + it("default factory", () => { + let i = 0 + const t = type({ bar: type("number[]").default(() => [++i]) }) + attest(t.assert({}).bar).snap([3]) + attest(t.assert({}).bar).snap([4]) + }) + it("default function factory", () => { + let i = 0 + const t = type({ + bar: type("Function").default(() => { + const j = ++i + return () => j + }) + }) + attest(t.assert({}).bar()).snap(3) + attest(t.assert({}).bar()).snap(4) + }) + }) }) diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index b102ab118..81d622e22 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -14,6 +14,7 @@ import type { ErrorMessage, inferred, Json, + Primitive, unset } from "@ark/util" import type { ArkAmbient } from "../config.ts" @@ -140,11 +141,20 @@ interface Type optional>(): instantiateType default< - const value extends this["inferIn"], + const value extends Extract< + this["inferIn"], + Primitive | Primitive[] | Record + >, r = applyConstraint> >( value: value ): instantiateType + default< + const value extends this["inferIn"], + r = applyConstraint> + >( + value: () => value + ): instantiateType // deprecate Function methods so they are deprioritized as suggestions From a37f917e2e63e916e0e70666aad9709d15ce689f Mon Sep 17 00:00:00 2001 From: Dimava Date: Wed, 18 Sep 2024 21:21:21 +0300 Subject: [PATCH 02/11] add default date mutability test --- ark/type/__tests__/defaults.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index 6ded98f57..4116f38c1 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -222,6 +222,15 @@ contextualize(() => { attest(t.json).equals(expected.json) }) + it("Date is immutable", () => { + const t = type({ date: 'Date = d"1993-05-21"' }) + const v1 = t.assert({}) + const time = v1.date.getTime() + v1.date.setMilliseconds(123) + const v2 = t.assert({}) + attest(v1.date.getTime()).equals(time) + }) + it("true", () => { const t = type({ key: "boolean = true" }) const expected = type({ key: ["boolean", "=", true] }) From 2a72e0d775960bbffe7c3c743bd88b75ebbfbd91 Mon Sep 17 00:00:00 2001 From: Dimava Date: Wed, 18 Sep 2024 21:29:20 +0300 Subject: [PATCH 03/11] simplify PR as much as it makes sense --- ark/schema/roots/root.ts | 2 +- ark/schema/structure/optional.ts | 51 +++-------------------------- ark/schema/structure/prop.ts | 37 +++++++++------------ ark/type/__tests__/defaults.test.ts | 37 +++++++-------------- ark/type/methods/base.ts | 5 +-- 5 files changed, 34 insertions(+), 98 deletions(-) diff --git a/ark/schema/roots/root.ts b/ark/schema/roots/root.ts index 4cc3fefbc..7f37a39da 100644 --- a/ark/schema/roots/root.ts +++ b/ark/schema/roots/root.ts @@ -324,7 +324,7 @@ export abstract class BaseRoot< } default(value: unknown): this { - value = assertDefaultValueAssignability(this, value, null, true) + assertDefaultValueAssignability(this, value) return this.withMeta({ default: value }) } diff --git a/ark/schema/structure/optional.ts b/ark/schema/structure/optional.ts index 9d76a7b94..d5f9f186f 100644 --- a/ark/schema/structure/optional.ts +++ b/ark/schema/structure/optional.ts @@ -65,8 +65,7 @@ export class OptionalNode extends BaseProp<"optional"> { assertDefaultValueAssignability( this.value, this.inner.default, - this.serializedKey, - false + this.serializedKey ) } } @@ -98,60 +97,18 @@ export const Optional = { const isPrimitive = (value: unknown): value is Primitive => typeof value === "object" ? value === null : typeof value !== "function" -const isSimpleSerializeable = (value: unknown): (() => unknown) | false => { - if (value instanceof Date) return () => new Date(value) - if ( - Array.isArray(value) && - Object.getPrototypeOf(value) === Array.prototype && - value.every(isPrimitive) - ) - return () => value.slice() - if ( - typeof value === "object" && - value !== null && - Object.getPrototypeOf(value) === Object.prototype && - Object.getOwnPropertySymbols(value).length === 0 && - Object.getOwnPropertyNames(value).every(k => { - const prop = Object.getOwnPropertyDescriptor(value, k) - return ( - prop && - "value" in prop && - isPrimitive(prop.value) && - prop.writable === true && - prop.enumerable === true && - prop.configurable === true - ) - }) - ) - return () => ({ ...value }) - return false -} export const assertDefaultValueAssignability = ( node: BaseRoot, value: unknown, - key: string | null, - canOverrideValue: boolean + key = "" ): unknown => { if (!isPrimitive(value) && typeof value !== "function") { - if (!canOverrideValue) { - throwParseError( - writeNonPrimitiveNonFunctionDefaultValueMessage(key ?? "", value) - ) - } - const fn = isSimpleSerializeable(value) - if (!fn) { - throwParseError( - writeNonPrimitiveNonFunctionDefaultValueMessage(key ?? "", value) - ) - } - value = fn + throwParseError(writeNonPrimitiveNonFunctionDefaultValueMessage(key, value)) } const out = node.in(typeof value === "function" ? value() : value) if (out instanceof ArkErrors) { - throwParseError( - writeUnassignableDefaultValueMessage(out.message, key ?? "") - ) + throwParseError(writeUnassignableDefaultValueMessage(out.message, key)) } return value } diff --git a/ark/schema/structure/prop.ts b/ark/schema/structure/prop.ts index 693828e7c..beaa8fde3 100644 --- a/ark/schema/structure/prop.ts +++ b/ark/schema/structure/prop.ts @@ -122,27 +122,13 @@ export abstract class BaseProp< return result } - private premorphedDefaultValue: unknown = - this.hasDefault() ? - this.hasDefaultFactory() ? - () => - this.value.includesMorph ? - this.value.assert(this.default()) - : this.default() - : this.value.includesMorph ? this.value.assert(this.default) - : this.default - : undefined + private morphedDefaultFactory: () => unknown = this.getMorphedFactory() private defaultValueMorphs: Morph[] = [ - this.hasDefaultFactory() ? - data => { - data[this.key] = (this.premorphedDefaultValue as () => unknown)() - return data - } - : data => { - data[this.key] = this.premorphedDefaultValue - return data - } + data => { + data[this.key] = this.morphedDefaultFactory() + return data + } ] private defaultValueMorphsReference = registeredReference( @@ -152,8 +138,17 @@ export abstract class BaseProp< hasDefault(): this is Optional.Node & { default: unknown } { return "default" in this } - hasDefaultFactory(): this is Optional.Node & { default: () => unknown } { - return this.hasDefault() && typeof this.default === "function" + getDefaultFactory(): () => unknown { + if (!this.hasDefault()) return () => undefined + if (typeof this.default === "function") return this.default as () => unknown + return () => this.default + } + getMorphedFactory(): () => unknown { + if (!this.hasDefault()) return () => undefined + const factory = this.getDefaultFactory() + return this.value.includesMorph ? + () => this.value.assert(factory()) + : factory } traverseAllows: TraverseAllows = (data, ctx) => { diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index 4116f38c1..575d0f461 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -375,55 +375,42 @@ contextualize(() => { }) describe("works with objects", () => { - it("default array in string", () => { - const t = type({ bar: type("number[] = []") }) - attest(t.assert({}).bar).snap([]) - attest(t.assert({}).bar !== t.assert({}).bar) - }) + // it("default array in string", () => { + // const t = type({ bar: type("number[] = []") }) + // attest(t.assert({}).bar).snap([]) + // attest(t.assert({}).bar !== t.assert({}).bar) + // }) it("default array", () => { const t = type({ - foo: type("number[]").default([1]), - bar: type("number[]").default(() => [1]), - baz: type("number[]") + foo: type("number[]").default(() => [1]), + bar: type("number[]") .pipe(v => v.map(e => e.toString())) .default(() => [1]) }) const v1 = t.assert({}), v2 = t.assert({}) - attest(v1).snap({ foo: [1], bar: [1], baz: ["1"] }) + attest(v1).snap({ foo: [1], bar: ["1"] }) attest(v1.foo !== v2.foo) }) it("default array is checked", () => { - attest(() => { - // @ts-expect-error - type({ foo: type("number[]").default(["a"]) }) - }).throws() attest(() => { // @ts-expect-error type({ bar: type("number[]").default(() => ["a"]) }) }).throws() attest(() => { - // @ts-expect-error type({ baz: type("number[]") .pipe(v => v.map(e => e.toString())) + // @ts-expect-error .default(() => ["a"]) }) }).throws() }) - it("disallows default array with non-primitive elements", () => { - ;[[], {}, new Date(), () => {}].forEach(v => { - attest(() => { - // @ts-expect-error - type({ foo: type("any[]").default(v) }) - }).throws() - }) - }) it("default object", () => { const t = type({ - foo: type({ "foo?": "string" }).default({}), + foo: type({ "foo?": "string" }).default(() => ({})), bar: type({ "foo?": "string" }).default(() => ({ foo: "foostr" })), - baz: type({ foo: "string = 'foostr'" }).default({}) + baz: type({ foo: "string = 'foostr'" }).default(() => ({})) }) const v1 = t.assert({}), v2 = t.assert({}) @@ -440,8 +427,8 @@ contextualize(() => { type({ foo: type({ foo: "string" }).default({}) }) }).throws() attest(() => { - // @ts-expect-error type({ + // @ts-expect-error bar: type({ foo: "number" }).default(() => ({ foo: "foostr" })) }) }).throws() diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index 81d622e22..79c95fdc0 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -141,10 +141,7 @@ interface Type optional>(): instantiateType default< - const value extends Extract< - this["inferIn"], - Primitive | Primitive[] | Record - >, + const value extends Extract, r = applyConstraint> >( value: value From 4ee7bd721e9e59aadd8e0dde5611a94aafef4707 Mon Sep 17 00:00:00 2001 From: Dimava Date: Wed, 18 Sep 2024 22:06:25 +0300 Subject: [PATCH 04/11] fix --- ark/schema/structure/optional.ts | 15 +++++++-------- ark/type/__tests__/defaults.test.ts | 2 +- ark/type/parser/shift/operator/default.ts | 7 +++++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/ark/schema/structure/optional.ts b/ark/schema/structure/optional.ts index d5f9f186f..858713e71 100644 --- a/ark/schema/structure/optional.ts +++ b/ark/schema/structure/optional.ts @@ -103,13 +103,13 @@ export const assertDefaultValueAssignability = ( value: unknown, key = "" ): unknown => { - if (!isPrimitive(value) && typeof value !== "function") { - throwParseError(writeNonPrimitiveNonFunctionDefaultValueMessage(key, value)) - } + if (!isPrimitive(value) && typeof value !== "function") + throwParseError(writeNonPrimitiveNonFunctionDefaultValueMessage(key)) + const out = node.in(typeof value === "function" ? value() : value) - if (out instanceof ArkErrors) { + if (out instanceof ArkErrors) throwParseError(writeUnassignableDefaultValueMessage(out.message, key)) - } + return value } @@ -124,7 +124,6 @@ export type writeUnassignableDefaultValueMessage< > = `Default value ${defaultValue} is not assignable to ${baseDef}` export const writeNonPrimitiveNonFunctionDefaultValueMessage = ( - key: string, - value: unknown + key: string ): string => - `Default value${key && ` for key ${key}`} is not primitive so it should be constructor function (was ${printable(value)})` + `Default value${key && ` for key ${key}`} is not primitive so it should be specified as a function like () => ({my: 'object'})` diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index 575d0f461..f2ccd966c 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -228,7 +228,7 @@ contextualize(() => { const time = v1.date.getTime() v1.date.setMilliseconds(123) const v2 = t.assert({}) - attest(v1.date.getTime()).equals(time) + attest(v2.date.getTime()).equals(time) }) it("true", () => { diff --git a/ark/type/parser/shift/operator/default.ts b/ark/type/parser/shift/operator/default.ts index 76d67b880..1d0c63953 100644 --- a/ark/type/parser/shift/operator/default.ts +++ b/ark/type/parser/shift/operator/default.ts @@ -27,8 +27,11 @@ export const parseDefault = (s: DynamicStateWithRoot): BaseRoot => { // token from which it was parsed if (!defaultNode.hasKind("unit")) return s.error(writeNonLiteralDefaultMessage(defaultNode.expression)) - - return baseNode.default(defaultNode.unit) + const defaultValue = + defaultNode.unit instanceof Date ? + () => new Date(defaultNode.unit as Date) + : defaultNode.unit + return baseNode.default(defaultValue) } export type parseDefault = From 97de97a929890509fa30014f63d4744bce848fa2 Mon Sep 17 00:00:00 2001 From: Dimava Date: Wed, 18 Sep 2024 23:54:31 +0300 Subject: [PATCH 05/11] fix types --- ark/type/__tests__/defaults.test.ts | 92 +++++++++++++++++++++++------ ark/type/keywords/inference.ts | 2 +- ark/type/parser/tuple.ts | 6 +- ark/type/type.ts | 8 ++- 4 files changed, 84 insertions(+), 24 deletions(-) diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index f2ccd966c..637b57197 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -1,5 +1,8 @@ import { attest, contextualize } from "@ark/attest" -import { writeUnassignableDefaultValueMessage } from "@ark/schema" +import { + writeNonPrimitiveNonFunctionDefaultValueMessage, + writeUnassignableDefaultValueMessage +} from "@ark/schema" import { scope, type } from "arktype" import type { Date } from "arktype/internal/keywords/constructors/Date.ts" import type { @@ -12,31 +15,48 @@ import { writeNonLiteralDefaultMessage } from "arktype/internal/parser/shift/ope contextualize(() => { describe("parsing and traversal", () => { it("base", () => { - const o = type({ foo: "string", bar: ["number", "=", 5] }) + const o = type({ + a: "string", + foo: "number = 5", + bar: ["number", "=", 5], + baz: ["number", "=", () => 5 as const] + }) + type x = typeof o.t.baz // ensure type ast displays is exactly as expected - attest(o.t).type.toString.snap("{ foo: string; bar: defaultsTo<5> }") - attest<{ foo: string; bar?: number }>(o.inferIn) - attest<{ foo: string; bar: number }>(o.infer) + attest(o.t).type.toString.snap( + "{ a: string; foo: defaultsTo<5>; bar: defaultsTo<5>; baz: defaultsTo<5> }" + ) + attest<{ a: string; foo?: number; bar?: number; baz?: number }>(o.inferIn) + attest<{ a: string; foo: number; bar: number; baz: number }>(o.infer) - attest(o.json).snap({ - required: [{ key: "foo", value: "string" }], + attest(o.omit("baz").json).snap({ + required: [{ key: "a", value: "string" }], optional: [ + // WHY ARE THEY REVERSED? { default: 5, key: "bar", value: { domain: "number", meta: { default: 5 } } + }, + { + default: 5, + key: "foo", + value: { domain: "number", meta: { default: 5 } } } ], domain: "object" }) - attest(o({ foo: "", bar: 4 })).equals({ foo: "", bar: 4 }) - attest(o({ foo: "" })).equals({ foo: "", bar: 5 }) - attest(o({ bar: 4 }).toString()).snap( - "foo must be a string (was missing)" - ) - attest(o({ foo: "", bar: "" }).toString()).snap( + attest(o({ a: "", foo: 4, bar: 4, baz: 4 })).equals({ + a: "", + foo: 4, + bar: 4, + baz: 4 + }) + attest(o({ a: "" })).equals({ a: "", foo: 5, bar: 5, baz: 5 }) + attest(o({ bar: 4 }).toString()).snap("a must be a string (was missing)") + attest(o({ a: "", bar: "" }).toString()).snap( "bar must be a number (was a string)" ) }) @@ -52,6 +72,16 @@ contextualize(() => { ) ) .type.errors.snap("Type 'string' is not assignable to type 'number'.") + attest(() => + // @ts-expect-error + type({ foo: "string", bar: ["number", "=", () => "5"] }) + ) + .throws( + writeUnassignableDefaultValueMessage( + "must be a number (was a string)" + ) + ) + .type.errors.snap("Type 'string' is not assignable to type 'number'.") }) it("validated default in scope", () => { @@ -212,14 +242,14 @@ contextualize(() => { const out = t.assert({}) // pass the same date instance back - const expected = type({ key: ["Date", "=", out.key] }) + const expected = type({ key: ["Date", "=", () => out.key] }) // we can't check expected here since the Date instance will not // have a narrowed literal type attest<{ key: InferredDefault> }>(t.t) - attest(t.json).equals(expected.json) + // attest(t.json).equals(expected.json) }) it("Date is immutable", () => { @@ -374,6 +404,23 @@ contextualize(() => { }) }) + describe("factory functions", () => { + it("works in tuple", () => { + const t = type({ foo: ["string", "=", () => "bar"] }) + attest(t.assert({ foo: "bar" })).snap({ foo: "bar" }) + }) + it("works in type tuple", () => { + const foo = type(["string", "=", () => "bar"]) + const t = type({ foo }) + attest(t.assert({ foo: "bar" })).snap({ foo: "bar" }) + }) + it("works in type args", () => { + const foo = type("string", "=", () => "bar") + const t = type({ foo }) + attest(t.assert({ foo: "bar" })).snap({ foo: "bar" }) + }) + }) + describe("works with objects", () => { // it("default array in string", () => { // const t = type({ bar: type("number[] = []") }) @@ -396,7 +443,10 @@ contextualize(() => { attest(() => { // @ts-expect-error type({ bar: type("number[]").default(() => ["a"]) }) - }).throws() + // THIS LOOKS WEIRD TBH + }).throws.snap( + "ParseError: Default value value at [0] must be a number (was a string)" + ) attest(() => { type({ baz: type("number[]") @@ -404,7 +454,9 @@ contextualize(() => { // @ts-expect-error .default(() => ["a"]) }) - }).throws() + }).throws.snap( + "ParseError: Default value value at [0] must be a number (was a string)" + ) }) it("default object", () => { const t = type({ @@ -425,13 +477,15 @@ contextualize(() => { attest(() => { // @ts-expect-error type({ foo: type({ foo: "string" }).default({}) }) - }).throws() + }).throws( + "ParseError: " + writeNonPrimitiveNonFunctionDefaultValueMessage("") + ) attest(() => { type({ // @ts-expect-error bar: type({ foo: "number" }).default(() => ({ foo: "foostr" })) }) - }).throws() + }).throws("ParseError: Default value foo must be a number (was a string)") }) it("default factory", () => { let i = 0 diff --git a/ark/type/keywords/inference.ts b/ark/type/keywords/inference.ts index a00e0741c..bc2306990 100644 --- a/ark/type/keywords/inference.ts +++ b/ark/type/keywords/inference.ts @@ -431,7 +431,7 @@ export type Optional = { export type InferredOptional = constrain export type Default = { - default?: { value: v } + default?: { value: v extends Primitive ? v | (() => v) : () => v } } export type InferredDefault = constrain> diff --git a/ark/type/parser/tuple.ts b/ark/type/parser/tuple.ts index d2732bbe9..e10207b2c 100644 --- a/ark/type/parser/tuple.ts +++ b/ark/type/parser/tuple.ts @@ -414,8 +414,10 @@ export type validateInfixExpression = : def[1] extends ":" ? Predicate> : def[1] extends "=>" ? Morph> : def[1] extends "@" ? MetaSchema - : def[1] extends "=" ? type.infer.Out - : validateDefinition + : def[1] extends "=" ? + | type.infer.Out + | (() => type.infer.Out) + : validateDefinition ] export type UnparsedTupleExpressionInput = { diff --git a/ark/type/type.ts b/ark/type/type.ts index 42bf45dfb..9aa19bd35 100644 --- a/ark/type/type.ts +++ b/ark/type/type.ts @@ -81,8 +81,12 @@ export interface TypeParser<$ = {}> extends Ark.boundTypeAttachments<$> { one extends ":" ? [Predicate>>] : one extends "=>" ? [Morph>, unknown>] : one extends "@" ? [MetaSchema] - : one extends "=" ? [distill.In, $>>] - : [type.validate] + : one extends "=" ? + [ + | distill.In, $>> + | (() => distill.In, $>>) + ] + : [type.validate] : [] ): r From de52eb3344deafe52679095448c0739a4cbaa8fb Mon Sep 17 00:00:00 2001 From: Dimava Date: Thu, 19 Sep 2024 00:15:20 +0300 Subject: [PATCH 06/11] fix tests --- ark/schema/structure/optional.ts | 3 +- ark/type/__tests__/defaults.test.ts | 54 ++++++++++++++++++++--------- ark/type/keywords/inference.ts | 5 ++- ark/type/parser/tuple.ts | 7 ++-- ark/type/type.ts | 10 ++---- 5 files changed, 50 insertions(+), 29 deletions(-) diff --git a/ark/schema/structure/optional.ts b/ark/schema/structure/optional.ts index 858713e71..ebe6e30bb 100644 --- a/ark/schema/structure/optional.ts +++ b/ark/schema/structure/optional.ts @@ -116,7 +116,8 @@ export const assertDefaultValueAssignability = ( export const writeUnassignableDefaultValueMessage = ( message: string, key = "" -): string => `Default value${key && ` for key ${key}`} ${message}` +): string => + `Default value${key && ` for key ${key}`} is not assignable: ${message}` export type writeUnassignableDefaultValueMessage< baseDef extends string, diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index 637b57197..11e5afe86 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -1,8 +1,10 @@ import { attest, contextualize } from "@ark/attest" import { + registeredReference, writeNonPrimitiveNonFunctionDefaultValueMessage, writeUnassignableDefaultValueMessage } from "@ark/schema" +import type { Primitive } from "@ark/util" import { scope, type } from "arktype" import type { Date } from "arktype/internal/keywords/constructors/Date.ts" import type { @@ -15,25 +17,35 @@ import { writeNonLiteralDefaultMessage } from "arktype/internal/parser/shift/ope contextualize(() => { describe("parsing and traversal", () => { it("base", () => { + const fn5 = () => 5 as const const o = type({ a: "string", foo: "number = 5", bar: ["number", "=", 5], - baz: ["number", "=", () => 5 as const] + baz: ["number", "=", fn5] }) - type x = typeof o.t.baz + const fn5reg = registeredReference(fn5) // ensure type ast displays is exactly as expected attest(o.t).type.toString.snap( - "{ a: string; foo: defaultsTo<5>; bar: defaultsTo<5>; baz: defaultsTo<5> }" + `{ + a: string + foo: defaultsTo<5> + bar: defaultsTo<5> + baz: defaultsTo<() => 5> +}` ) attest<{ a: string; foo?: number; bar?: number; baz?: number }>(o.inferIn) attest<{ a: string; foo: number; bar: number; baz: number }>(o.infer) - attest(o.omit("baz").json).snap({ + attest(o.json).snap({ required: [{ key: "a", value: "string" }], optional: [ - // WHY ARE THEY REVERSED? + { + default: fn5reg, + key: "baz", + value: { domain: "number", meta: { default: fn5reg } } + }, { default: 5, key: "bar", @@ -71,7 +83,9 @@ contextualize(() => { "must be a number (was a string)" ) ) - .type.errors.snap("Type 'string' is not assignable to type 'number'.") + .type.errors.snap( + "Type 'string' is not assignable to type 'number | (() => number)'." + ) attest(() => // @ts-expect-error type({ foo: "string", bar: ["number", "=", () => "5"] }) @@ -81,7 +95,9 @@ contextualize(() => { "must be a number (was a string)" ) ) - .type.errors.snap("Type 'string' is not assignable to type 'number'.") + .type.errors.snap( + "Type '() => string' is not assignable to type 'number | (() => number)'.Type '() => string' is not assignable to type '() => number'.Type 'string' is not assignable to type 'number'." + ) }) it("validated default in scope", () => { @@ -163,7 +179,7 @@ contextualize(() => { writeUnassignableDefaultValueMessage("must be a number (was boolean)") ) .type.errors( - "'boolean' is not assignable to parameter of type 'number'" + "'boolean' is not assignable to parameter of type 'number | (() => number)'" ) }) @@ -444,8 +460,10 @@ contextualize(() => { // @ts-expect-error type({ bar: type("number[]").default(() => ["a"]) }) // THIS LOOKS WEIRD TBH - }).throws.snap( - "ParseError: Default value value at [0] must be a number (was a string)" + }).throws( + writeUnassignableDefaultValueMessage( + "value at [0] must be a number (was a string)" + ) ) attest(() => { type({ @@ -454,8 +472,10 @@ contextualize(() => { // @ts-expect-error .default(() => ["a"]) }) - }).throws.snap( - "ParseError: Default value value at [0] must be a number (was a string)" + }).throws( + writeUnassignableDefaultValueMessage( + "value at [0] must be a number (was a string)" + ) ) }) it("default object", () => { @@ -477,15 +497,17 @@ contextualize(() => { attest(() => { // @ts-expect-error type({ foo: type({ foo: "string" }).default({}) }) - }).throws( - "ParseError: " + writeNonPrimitiveNonFunctionDefaultValueMessage("") - ) + }).throws(writeNonPrimitiveNonFunctionDefaultValueMessage("")) attest(() => { type({ // @ts-expect-error bar: type({ foo: "number" }).default(() => ({ foo: "foostr" })) }) - }).throws("ParseError: Default value foo must be a number (was a string)") + }).throws( + writeUnassignableDefaultValueMessage( + "foo must be a number (was a string)" + ) + ) }) it("default factory", () => { let i = 0 diff --git a/ark/type/keywords/inference.ts b/ark/type/keywords/inference.ts index bc2306990..50fc04e1b 100644 --- a/ark/type/keywords/inference.ts +++ b/ark/type/keywords/inference.ts @@ -431,9 +431,12 @@ export type Optional = { export type InferredOptional = constrain export type Default = { - default?: { value: v extends Primitive ? v | (() => v) : () => v } + default?: { value: v } } +export type DefaultFor = + t extends Primitive ? t | (() => t) : (t & Primitive) | (() => t) + export type InferredDefault = constrain> export type termOrType = t | Type diff --git a/ark/type/parser/tuple.ts b/ark/type/parser/tuple.ts index e10207b2c..90737b0ff 100644 --- a/ark/type/parser/tuple.ts +++ b/ark/type/parser/tuple.ts @@ -29,6 +29,7 @@ import { import type { applyConstraint, Default, + DefaultFor, distill, inferIntersection, inferMorphOut, @@ -414,10 +415,8 @@ export type validateInfixExpression = : def[1] extends ":" ? Predicate> : def[1] extends "=>" ? Morph> : def[1] extends "@" ? MetaSchema - : def[1] extends "=" ? - | type.infer.Out - | (() => type.infer.Out) - : validateDefinition + : def[1] extends "=" ? DefaultFor> + : validateDefinition ] export type UnparsedTupleExpressionInput = { diff --git a/ark/type/type.ts b/ark/type/type.ts index 9aa19bd35..a4838dae9 100644 --- a/ark/type/type.ts +++ b/ark/type/type.ts @@ -23,7 +23,7 @@ import type { parseValidGenericParams, validateParameterString } from "./generic.ts" -import type { distill } from "./keywords/inference.ts" +import type { DefaultFor, distill } from "./keywords/inference.ts" import type { Ark, keywords, type } from "./keywords/keywords.ts" import type { BaseType } from "./methods/base.ts" import type { instantiateType } from "./methods/instantiate.ts" @@ -81,12 +81,8 @@ export interface TypeParser<$ = {}> extends Ark.boundTypeAttachments<$> { one extends ":" ? [Predicate>>] : one extends "=>" ? [Morph>, unknown>] : one extends "@" ? [MetaSchema] - : one extends "=" ? - [ - | distill.In, $>> - | (() => distill.In, $>>) - ] - : [type.validate] + : one extends "=" ? [DefaultFor, $>>>] + : [type.validate] : [] ): r From 7250abc25846debf094cdcf59d3d986c07d81893 Mon Sep 17 00:00:00 2001 From: Dimava Date: Thu, 19 Sep 2024 01:28:34 +0300 Subject: [PATCH 07/11] add more test and fix anys --- ark/attest/config.ts | 2 +- ark/type/__tests__/defaults.test.ts | 114 +++++++++++++++++++++++++++- ark/type/keywords/inference.ts | 6 +- 3 files changed, 116 insertions(+), 6 deletions(-) diff --git a/ark/attest/config.ts b/ark/attest/config.ts index 360d2d0e3..d18f78372 100644 --- a/ark/attest/config.ts +++ b/ark/attest/config.ts @@ -63,7 +63,7 @@ export const getDefaultAttestConfig = (): BaseAttestConfig => ({ existsSync(fromCwd("tsconfig.json")) ? fromCwd("tsconfig.json") : undefined, compilerOptions: {}, attestAliases: ["attest", "attestInternal"], - updateSnapshots: false, + updateSnapshots: !false, skipTypes: false, skipInlineInstantiations: false, tsVersions: "default", diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index 11e5afe86..84e6607c9 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -27,14 +27,12 @@ contextualize(() => { const fn5reg = registeredReference(fn5) // ensure type ast displays is exactly as expected - attest(o.t).type.toString.snap( - `{ + attest(o.t).type.toString.snap(`{ a: string foo: defaultsTo<5> bar: defaultsTo<5> baz: defaultsTo<() => 5> -}` - ) +}`) attest<{ a: string; foo?: number; bar?: number; baz?: number }>(o.inferIn) attest<{ a: string; foo: number; bar: number; baz: number }>(o.infer) @@ -375,6 +373,114 @@ contextualize(() => { }) }) + describe("allows assigning primitives to whatever", () => { + it("allows primitives, subtypes and functions for anys", () => { + const fn = () => {} + const t = type({ + foo1: ["unknown", "=", true], + bar1: ["unknown", "=", () => [true]], + baz1: ["unknown", "=", () => fn], + foo2: ["unknown.any", "=", true], + bar2: ["unknown.any", "=", () => [true]], + baz2: ["unknown.any", "=", () => fn] + }) + const v = t.assert({}) + attest(v).snap({ + foo1: true, + bar1: [true], + baz1: fn, + foo2: true, + bar2: [true], + baz2: fn + }) + }) + it("disallows plain objects for anys", () => { + attest(() => { + // @ts-expect-error + type({ foo: ["unknown", "=", { foo: "bar" }] }) + }) + .throws.snap( + "ParseError: Default value is not primitive so it should be specified as a function like () => ({my: 'object'})" + ) + .type.errors.snap( + "Object literal may only specify known properties, and 'foo' does not exist in type '() => unknown'." + ) + attest(() => { + // @ts-expect-error + type({ foo: ["unknown.any", "=", { foo: "bar" }] }) + }) + .throws.snap( + "ParseError: Default value is not primitive so it should be specified as a function like () => ({my: 'object'})" + ) + .type.errors.snap( + "Object literal may only specify known properties, and 'foo' does not exist in type '(() => any) | (() => any)'." + ) + }) + it("allows string sybtyping", () => { + type({ + foo: [/^foo/ as type.cast<`foo${string}`>, "=", "foobar"], + bar: [/bar$/ as type.cast<`${string}bar`>, "=", () => "foobar" as const] + }) + }) + it("shows types plainly", () => { + attest( + // @ts-expect-error + () => type({ foo: ["number", "=", true] }) + ) + .throws() + .type.errors.snap( + "Type 'boolean' is not assignable to type 'number | (() => number)'." + ) + attest( + // @ts-expect-error + () => type({ foo: ["number[]", "=", true] }) + ) + .throws() + .type.errors.snap( + "Type 'boolean' is not assignable to type '() => number[]'." + ) + attest(() => + type({ + // @ts-expect-error + foo: [/foo/ as type.cast<"string" & { foo: true }>, "=", true] + }) + ) + .throws() + .type.errors.snap( + "Type 'true' is not assignable to type '(\"string\" & { foo: true; }) | (() => \"string\" & { foo: true; })'." + ) + attest( + // @ts-expect-error + () => type({ foo: [["number[]", "|", "number"], "=", true] }) + ) + .throws() + .type.errors.snap( + "Type 'boolean' is not assignable to type 'number | (() => number) | (() => number[])'." + ) + }) + it("only allows argless functions for factories", () => { + attest(() => { + // @ts-expect-error + type({ bar: ["Function", "=", class {}] }) + }) + .throws.snap( + "TypeError: Class constructors cannot be invoked without 'new'" + ) + .type.errors.snap( + "Type 'typeof (Anonymous class)' is not assignable to type '() => Function'.Type 'typeof (Anonymous class)' provides no match for the signature '(): Function'." + ) + attest(() => { + // @ts-expect-error + type({ bar: ["number", "=", (a: number) => 1] }) + }).type.errors.snap( + "Type '(a: number) => number' is not assignable to type 'number | (() => number)'.Type '(a: number) => number' is not assignable to type '() => number'.Target signature provides too few arguments. Expected 1 or more, but got 0." + ) + attest(() => { + type({ bar: ["number", "=", (a?: number) => 1] }) + }) + }) + }) + describe("intersection", () => { it("two optionals, one default", () => { const l = type({ bar: ["number", "=", 5] }) diff --git a/ark/type/keywords/inference.ts b/ark/type/keywords/inference.ts index 50fc04e1b..6f248fecd 100644 --- a/ark/type/keywords/inference.ts +++ b/ark/type/keywords/inference.ts @@ -435,7 +435,11 @@ export type Default = { } export type DefaultFor = - t extends Primitive ? t | (() => t) : (t & Primitive) | (() => t) + t extends Primitive ? (0 extends 1 & t ? Primitive : t) | (() => t) + : | (Primitive extends t ? Primitive + : t extends Primitive ? t + : never) + | (() => t) export type InferredDefault = constrain> From 93a9e67fcc1835b475bab67afb443648807d5ce1 Mon Sep 17 00:00:00 2001 From: Dimava Date: Thu, 19 Sep 2024 01:35:06 +0300 Subject: [PATCH 08/11] revert snapping --- ark/attest/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ark/attest/config.ts b/ark/attest/config.ts index d18f78372..360d2d0e3 100644 --- a/ark/attest/config.ts +++ b/ark/attest/config.ts @@ -63,7 +63,7 @@ export const getDefaultAttestConfig = (): BaseAttestConfig => ({ existsSync(fromCwd("tsconfig.json")) ? fromCwd("tsconfig.json") : undefined, compilerOptions: {}, attestAliases: ["attest", "attestInternal"], - updateSnapshots: !false, + updateSnapshots: false, skipTypes: false, skipInlineInstantiations: false, tsVersions: "default", From 1dcb0a562be95ef629aca05036b32a71dca0cdf5 Mon Sep 17 00:00:00 2001 From: Dimava Date: Thu, 19 Sep 2024 11:59:58 +0300 Subject: [PATCH 09/11] use inferIn for defaults, fix infinitely deep, add more tests --- ark/type/__tests__/defaults.test.ts | 212 +++++++++++++++++++++------- ark/type/keywords/inference.ts | 2 +- ark/type/methods/base.ts | 4 +- ark/type/parser/ast/default.ts | 4 +- ark/type/parser/tuple.ts | 2 +- 5 files changed, 168 insertions(+), 56 deletions(-) diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index 84e6607c9..1841e2ee3 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -373,8 +373,8 @@ contextualize(() => { }) }) - describe("allows assigning primitives to whatever", () => { - it("allows primitives, subtypes and functions for anys", () => { + describe("works properly with types", () => { + it("allows primitives and factories for anys", () => { const fn = () => {} const t = type({ foo1: ["unknown", "=", true], @@ -399,29 +399,23 @@ contextualize(() => { // @ts-expect-error type({ foo: ["unknown", "=", { foo: "bar" }] }) }) - .throws.snap( - "ParseError: Default value is not primitive so it should be specified as a function like () => ({my: 'object'})" - ) - .type.errors.snap( - "Object literal may only specify known properties, and 'foo' does not exist in type '() => unknown'." - ) + .throws("is not primitive") + .type.errors("'foo' does not exist in type '() => unknown'.") attest(() => { // @ts-expect-error type({ foo: ["unknown.any", "=", { foo: "bar" }] }) }) - .throws.snap( - "ParseError: Default value is not primitive so it should be specified as a function like () => ({my: 'object'})" - ) - .type.errors.snap( - "Object literal may only specify known properties, and 'foo' does not exist in type '(() => any) | (() => any)'." - ) + .throws("is not primitive") + .type.errors("'foo' does not exist in type '() => any'.") }) + it("allows string sybtyping", () => { type({ foo: [/^foo/ as type.cast<`foo${string}`>, "=", "foobar"], bar: [/bar$/ as type.cast<`${string}bar`>, "=", () => "foobar" as const] }) }) + it("shows types plainly", () => { attest( // @ts-expect-error @@ -439,6 +433,14 @@ contextualize(() => { .type.errors.snap( "Type 'boolean' is not assignable to type '() => number[]'." ) + attest( + // @ts-expect-error + () => type({ foo: [{ bar: "false" }, "=", true] }) + ) + .throws() + .type.errors.snap( + "Type 'boolean' is not assignable to type '() => { bar: false; }'." + ) attest(() => type({ // @ts-expect-error @@ -451,32 +453,88 @@ contextualize(() => { ) attest( // @ts-expect-error - () => type({ foo: [["number[]", "|", "number"], "=", true] }) + () => type({ foo: [["number[]", "|", "string"], "=", true] }) ) .throws() .type.errors.snap( - "Type 'boolean' is not assignable to type 'number | (() => number) | (() => number[])'." + "Type 'boolean' is not assignable to type 'string | (() => string | number[])'." ) - }) - it("only allows argless functions for factories", () => { - attest(() => { + attest( // @ts-expect-error - type({ bar: ["Function", "=", class {}] }) - }) - .throws.snap( - "TypeError: Class constructors cannot be invoked without 'new'" - ) + () => type(["number[]", "|", "string"], "=", true) + ) + .throws() .type.errors.snap( - "Type 'typeof (Anonymous class)' is not assignable to type '() => Function'.Type 'typeof (Anonymous class)' provides no match for the signature '(): Function'." + "Argument of type 'boolean' is not assignable to parameter of type 'string | (() => string | number[])'." ) - attest(() => { + // should not cause "instantiation is excessively deep" + attest( // @ts-expect-error - type({ bar: ["number", "=", (a: number) => 1] }) - }).type.errors.snap( - "Type '(a: number) => number' is not assignable to type 'number | (() => number)'.Type '(a: number) => number' is not assignable to type '() => number'.Target signature provides too few arguments. Expected 1 or more, but got 0." + () => type("number[]", "|", "string").default(true) ) - attest(() => { - type({ bar: ["number", "=", (a?: number) => 1] }) + .throws() + .type.errors(/of type 'string'.*of type '\(\) => string | number\[\]'/) + // should not cause "instantiation is excessively deep" + attest( + // @ts-expect-error + () => type("number[]", "|", "string").default(() => true) + ) + .throws() + .type.errors(/'of type 'string'.*of type '\(\) => string | number\[\]'/) + }) + + it("uses input type for morphs", () => { + // @ts-expect-error + attest(() => type({ foo: ["string.numeric.parse = true"] })) + .throws("must be a string (was boolean)") + .type.errors( + "Default value true is not assignable to string.numeric.parse" + ) + // @ts-expect-error + attest(() => type({ foo: ["string.numeric.parse", "=", true] })) + .throws("must be a string (was boolean)") + .type.errors.snap( + "Type 'boolean' is not assignable to type 'string | (() => string)'." + ) + // @ts-expect-error + attest(() => type({ foo: ["string.numeric.parse", "=", () => true] })) + .throws("must be a string (was boolean)") + .type.errors( + "Type '() => boolean' is not assignable to type 'string | (() => string)'." + ) + const numtos = type("number").pipe(s => `${s}`) + // @ts-expect-error + attest(() => type({ foo: [numtos, "=", true] })) + .throws("must be a number (was boolean)") + .type.errors( + "Type 'boolean' is not assignable to type 'number | (() => number)'." + ) + // @ts-expect-error + attest(() => type({ foo: [numtos, "=", () => true] })) + .throws("must be a number (was boolean)") + .type.errors( + "Type '() => boolean' is not assignable to type 'number | (() => number)'." + ) + + const f = type({ + foo1: "string.numeric.parse = '123'", + foo2: ["string.numeric.parse", "=", "123"], + foo3: ["string.numeric.parse", "=", () => "123"], + bar1: [numtos, "=", 123], + bar2: [numtos, "=", () => 123], + baz1: type(numtos, "=", 123), + baz2: type(numtos, "=", () => 123), + baz3: type(numtos).default(123) + }) + attest(f.assert({})).snap({ + foo1: 123, + foo2: 123, + foo3: 123, + bar1: "123", + bar2: "123", + baz1: "123", + baz2: "123", + baz3: "123" }) }) }) @@ -531,19 +589,91 @@ contextualize(() => { const t = type({ foo: ["string", "=", () => "bar"] }) attest(t.assert({ foo: "bar" })).snap({ foo: "bar" }) }) + it("works in type tuple", () => { const foo = type(["string", "=", () => "bar"]) const t = type({ foo }) attest(t.assert({ foo: "bar" })).snap({ foo: "bar" }) }) + it("works in type args", () => { const foo = type("string", "=", () => "bar") const t = type({ foo }) attest(t.assert({ foo: "bar" })).snap({ foo: "bar" }) }) + + it("checks the returned value", () => { + attest(() => { + // @ts-expect-error + type({ foo: ["number", "=", () => "bar"] }) + }).throws.snap( + "ParseError: Default value is not assignable: must be a number (was a string)" + ) + attest(() => { + // @ts-expect-error + type({ foo: ["number[]", "=", () => "bar"] }) + }).throws.snap( + "ParseError: Default value is not assignable: must be an array (was string)" + ) + }) + + it("morphs the returned value", () => { + const t = type({ foo: ["string.numeric.parse", "=", () => "123"] }) + attest(t.assert({})).snap({ foo: 123 }) + }) + + it("only allows argless functions for factories", () => { + attest(() => { + // @ts-expect-error + type({ bar: ["Function", "=", class {}] }) + }) + .throws.snap( + "TypeError: Class constructors cannot be invoked without 'new'" + ) + .type.errors.snap( + "Type 'typeof (Anonymous class)' is not assignable to type '() => Function'.Type 'typeof (Anonymous class)' provides no match for the signature '(): Function'." + ) + attest(() => { + // @ts-expect-error + type({ bar: ["number", "=", (a: number) => 1] }) + }).type.errors.snap( + "Type '(a: number) => number' is not assignable to type 'number | (() => number)'.Type '(a: number) => number' is not assignable to type '() => number'.Target signature provides too few arguments. Expected 1 or more, but got 0." + ) + attest(() => { + type({ bar: ["number", "=", (a?: number) => 1] }) + }) + }) + + it("default factory may return different values", () => { + let i = 0 + const t = type({ bar: type("number[]").default(() => [++i]) }) + attest(t.assert({}).bar).snap([3]) + attest(t.assert({}).bar).snap([4]) + }) + + it("default function factory", () => { + let i = 0 + const t = type({ + bar: type("Function").default(() => { + const j = ++i + return () => j + }) + }) + attest(t.assert({}).bar()).snap(3) + attest(t.assert({}).bar()).snap(4) + }) + + it("allows union factory", () => { + let i = 0 + const t = type({ + foo: [["number", "|", "number[]"], "=", () => (i % 2 ? ++i : [++i])] + }) + attest(t.assert({})).snap({ foo: [3] }) + attest(t.assert({})).snap({ foo: 4 }) + }) }) - describe("works with objects", () => { + describe("works with factories", () => { // it("default array in string", () => { // const t = type({ bar: type("number[] = []") }) // attest(t.assert({}).bar).snap([]) @@ -565,7 +695,6 @@ contextualize(() => { attest(() => { // @ts-expect-error type({ bar: type("number[]").default(() => ["a"]) }) - // THIS LOOKS WEIRD TBH }).throws( writeUnassignableDefaultValueMessage( "value at [0] must be a number (was a string)" @@ -615,22 +744,5 @@ contextualize(() => { ) ) }) - it("default factory", () => { - let i = 0 - const t = type({ bar: type("number[]").default(() => [++i]) }) - attest(t.assert({}).bar).snap([3]) - attest(t.assert({}).bar).snap([4]) - }) - it("default function factory", () => { - let i = 0 - const t = type({ - bar: type("Function").default(() => { - const j = ++i - return () => j - }) - }) - attest(t.assert({}).bar()).snap(3) - attest(t.assert({}).bar()).snap(4) - }) }) }) diff --git a/ark/type/keywords/inference.ts b/ark/type/keywords/inference.ts index 6f248fecd..49b2ce99f 100644 --- a/ark/type/keywords/inference.ts +++ b/ark/type/keywords/inference.ts @@ -435,7 +435,7 @@ export type Default = { } export type DefaultFor = - t extends Primitive ? (0 extends 1 & t ? Primitive : t) | (() => t) + [t] extends [Primitive] ? (0 extends 1 & t ? Primitive : t) | (() => t) : | (Primitive extends t ? Primitive : t extends Primitive ? t : never) diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index 79c95fdc0..b1ee0f4cb 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -145,13 +145,13 @@ interface Type r = applyConstraint> >( value: value - ): instantiateType + ): NoInfer> default< const value extends this["inferIn"], r = applyConstraint> >( value: () => value - ): instantiateType + ): NoInfer> // deprecate Function methods so they are deprioritized as suggestions diff --git a/ark/type/parser/ast/default.ts b/ark/type/parser/ast/default.ts index fbe05d201..a88edba5c 100644 --- a/ark/type/parser/ast/default.ts +++ b/ark/type/parser/ast/default.ts @@ -2,7 +2,7 @@ import type { writeUnassignableDefaultValueMessage } from "@ark/schema" import type { ErrorMessage } from "@ark/util" import type { type } from "../../keywords/keywords.ts" import type { UnitLiteral } from "../shift/operator/default.ts" -import type { inferAstOut } from "./infer.ts" +import type { inferAstIn } from "./infer.ts" import type { astToString } from "./utils.ts" import type { validateAst } from "./validate.ts" @@ -10,7 +10,7 @@ export type validateDefault = validateAst extends infer e extends ErrorMessage ? e : // check against the output of the type since morphs will not occur // ambient infer is safe since the default value is always a literal - type.infer extends inferAstOut ? undefined + type.infer extends inferAstIn ? undefined : ErrorMessage< writeUnassignableDefaultValueMessage, unitLiteral> > diff --git a/ark/type/parser/tuple.ts b/ark/type/parser/tuple.ts index 90737b0ff..c64b4decd 100644 --- a/ark/type/parser/tuple.ts +++ b/ark/type/parser/tuple.ts @@ -415,7 +415,7 @@ export type validateInfixExpression = : def[1] extends ":" ? Predicate> : def[1] extends "=>" ? Morph> : def[1] extends "@" ? MetaSchema - : def[1] extends "=" ? DefaultFor> + : def[1] extends "=" ? DefaultFor> : validateDefinition ] From f770f50da6d0ad04901e29344969df7f379bf0b6 Mon Sep 17 00:00:00 2001 From: Dimava Date: Thu, 19 Sep 2024 15:54:18 +0300 Subject: [PATCH 10/11] finalize --- ark/schema/structure/optional.ts | 13 ++++++++----- ark/type/__tests__/defaults.test.ts | 19 +++++++++++++++---- ark/type/methods/base.ts | 9 ++------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/ark/schema/structure/optional.ts b/ark/schema/structure/optional.ts index ebe6e30bb..5b6778b25 100644 --- a/ark/schema/structure/optional.ts +++ b/ark/schema/structure/optional.ts @@ -1,4 +1,10 @@ -import { printable, throwParseError, unset, type Primitive } from "@ark/util" +import { + hasDomain, + printable, + throwParseError, + unset, + type Primitive +} from "@ark/util" import type { BaseRoot } from "../roots/root.ts" import type { declareNode } from "../shared/declare.ts" import { ArkErrors } from "../shared/errors.ts" @@ -95,15 +101,12 @@ export const Optional = { Node: OptionalNode } -const isPrimitive = (value: unknown): value is Primitive => - typeof value === "object" ? value === null : typeof value !== "function" - export const assertDefaultValueAssignability = ( node: BaseRoot, value: unknown, key = "" ): unknown => { - if (!isPrimitive(value) && typeof value !== "function") + if (hasDomain(value, "object") && typeof value !== "function") throwParseError(writeNonPrimitiveNonFunctionDefaultValueMessage(key)) const out = node.in(typeof value === "function" ? value() : value) diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index 1841e2ee3..124a60e87 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -158,7 +158,7 @@ contextualize(() => { writeUnassignableDefaultValueMessage("must be a number (was boolean)") ) .type.errors( - "'boolean' is not assignable to parameter of type 'number'" + "'boolean' is not assignable to parameter of type 'number | (() => number)'" ) }) @@ -473,14 +473,18 @@ contextualize(() => { () => type("number[]", "|", "string").default(true) ) .throws() - .type.errors(/of type 'string'.*of type '\(\) => string | number\[\]'/) + .type.errors( + "not assignable to parameter of type 'string | (() => string | number[])'." + ) // should not cause "instantiation is excessively deep" attest( // @ts-expect-error () => type("number[]", "|", "string").default(() => true) ) .throws() - .type.errors(/'of type 'string'.*of type '\(\) => string | number\[\]'/) + .type.errors( + "not assignable to parameter of type 'string | (() => string | number[])'." + ) }) it("uses input type for morphs", () => { @@ -615,6 +619,12 @@ contextualize(() => { }).throws.snap( "ParseError: Default value is not assignable: must be an array (was string)" ) + attest(() => { + // @ts-expect-error + type({ foo: [{ a: "number" }, "=", () => ({ a: "bar" })] }) + }).throws.snap( + "ParseError: Default value is not assignable: a must be a number (was a string)" + ) }) it("morphs the returned value", () => { @@ -654,7 +664,8 @@ contextualize(() => { it("default function factory", () => { let i = 0 const t = type({ - bar: type("Function").default(() => { + // this requires explicit type argument + bar: type("Function").default<() => number>(() => { const j = ++i return () => j }) diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index b1ee0f4cb..e9a9c8796 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -21,6 +21,7 @@ import type { ArkAmbient } from "../config.ts" import type { applyConstraint, Default, + DefaultFor, distill, inferIntersection, inferMorphOut, @@ -140,17 +141,11 @@ interface Type // work the way it does for the other methods here optional>(): instantiateType - default< - const value extends Extract, - r = applyConstraint> - >( - value: value - ): NoInfer> default< const value extends this["inferIn"], r = applyConstraint> >( - value: () => value + value: DefaultFor ): NoInfer> // deprecate Function methods so they are deprioritized as suggestions From 296babb14a0f104be0217957eea1529179a306fa Mon Sep 17 00:00:00 2001 From: Dimava Date: Thu, 19 Sep 2024 16:00:54 +0300 Subject: [PATCH 11/11] remove unused --- ark/schema/structure/optional.ts | 8 +------- ark/type/methods/base.ts | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/ark/schema/structure/optional.ts b/ark/schema/structure/optional.ts index 5b6778b25..b095067fd 100644 --- a/ark/schema/structure/optional.ts +++ b/ark/schema/structure/optional.ts @@ -1,10 +1,4 @@ -import { - hasDomain, - printable, - throwParseError, - unset, - type Primitive -} from "@ark/util" +import { hasDomain, printable, throwParseError, unset } from "@ark/util" import type { BaseRoot } from "../roots/root.ts" import type { declareNode } from "../shared/declare.ts" import { ArkErrors } from "../shared/errors.ts" diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index e9a9c8796..b20871d32 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -14,7 +14,6 @@ import type { ErrorMessage, inferred, Json, - Primitive, unset } from "@ark/util" import type { ArkAmbient } from "../config.ts"