From 374c60b627ef2a2d667239999fb1df6d7d7ffb14 Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 20 Sep 2024 01:55:49 -0400 Subject: [PATCH] feat: improve default morphs, simplify attribute types (#1142) --- ark/attest/package.json | 2 +- ark/fs/package.json | 2 +- ark/schema/index.ts | 1 + ark/schema/node.ts | 18 +- ark/schema/package.json | 2 +- ark/schema/roots/root.ts | 26 +- ark/schema/shared/implement.ts | 7 +- ark/schema/shared/intersections.ts | 41 ++- ark/schema/structure/optional.ts | 98 +++++-- ark/schema/structure/prop.ts | 35 +-- ark/schema/structure/structure.ts | 28 +- ark/type/__tests__/cast.test.ts | 17 ++ ark/type/__tests__/defaults.test.ts | 284 ++++++++++++++++--- ark/type/__tests__/get.test.ts | 6 +- ark/type/__tests__/imports.test.ts | 12 +- ark/type/__tests__/narrow.test.ts | 8 +- ark/type/__tests__/objects/namedKeys.test.ts | 30 +- ark/type/__tests__/pipe.test.ts | 4 +- ark/type/__tests__/realWorld.test.ts | 6 +- ark/type/keywords/constructors/Date.ts | 39 ++- ark/type/keywords/inference.ts | 169 ++++------- ark/type/keywords/number/number.ts | 54 ++-- ark/type/keywords/string/alpha.ts | 4 +- ark/type/keywords/string/alphanumeric.ts | 4 +- ark/type/keywords/string/capitalize.ts | 4 +- ark/type/keywords/string/creditCard.ts | 4 +- ark/type/keywords/string/date.ts | 17 +- ark/type/keywords/string/digits.ts | 4 +- ark/type/keywords/string/email.ts | 4 +- ark/type/keywords/string/integer.ts | 8 +- ark/type/keywords/string/ip.ts | 8 +- ark/type/keywords/string/json.ts | 8 +- ark/type/keywords/string/lower.ts | 4 +- ark/type/keywords/string/normalize.ts | 10 +- ark/type/keywords/string/numeric.ts | 4 +- ark/type/keywords/string/semver.ts | 4 +- ark/type/keywords/string/string.ts | 50 ++-- ark/type/keywords/string/trim.ts | 4 +- ark/type/keywords/string/upper.ts | 4 +- ark/type/keywords/string/url.ts | 8 +- ark/type/keywords/string/uuid.ts | 20 +- ark/type/methods/base.ts | 31 +- ark/type/methods/object.ts | 25 +- ark/type/package.json | 2 +- ark/type/parser/ast/infer.ts | 26 +- ark/type/parser/ast/validate.ts | 28 +- ark/type/parser/objectLiteral.ts | 4 +- ark/type/parser/reduce/dynamic.ts | 3 +- ark/type/parser/shift/operand/unenclosed.ts | 18 +- ark/type/parser/shift/operator/brand.ts | 19 ++ ark/type/parser/shift/operator/operator.ts | 3 + ark/type/parser/shift/scanner.ts | 16 +- ark/type/parser/tuple.ts | 11 +- ark/util/package.json | 2 +- ark/util/registry.ts | 2 +- 55 files changed, 805 insertions(+), 447 deletions(-) create mode 100644 ark/type/parser/shift/operator/brand.ts diff --git a/ark/attest/package.json b/ark/attest/package.json index 21e3c25f4..780343b41 100644 --- a/ark/attest/package.json +++ b/ark/attest/package.json @@ -1,6 +1,6 @@ { "name": "@ark/attest", - "version": "0.18.4", + "version": "0.19.0", "author": { "name": "David Blass", "email": "david@arktype.io", diff --git a/ark/fs/package.json b/ark/fs/package.json index 4a2aebe55..db7a705bf 100644 --- a/ark/fs/package.json +++ b/ark/fs/package.json @@ -1,6 +1,6 @@ { "name": "@ark/fs", - "version": "0.13.0", + "version": "0.14.0", "author": { "name": "David Blass", "email": "david@arktype.io", diff --git a/ark/schema/index.ts b/ark/schema/index.ts index 7c2f4bbf1..d53a8a9d7 100644 --- a/ark/schema/index.ts +++ b/ark/schema/index.ts @@ -33,6 +33,7 @@ export * from "./shared/implement.ts" export * from "./shared/intersections.ts" export * from "./shared/jsonSchema.ts" export * from "./shared/registry.ts" +export * from "./shared/traversal.ts" export * from "./shared/utils.ts" export * from "./structure/index.ts" export * from "./structure/optional.ts" diff --git a/ark/schema/node.ts b/ark/schema/node.ts index e0a127c79..b5109f603 100644 --- a/ark/schema/node.ts +++ b/ark/schema/node.ts @@ -61,14 +61,15 @@ export abstract class BaseNode< /** uses -ignore rather than -expect-error because this is not an error in .d.ts * @ts-ignore allow instantiation assignment to the base type */ out d extends BaseNodeDeclaration = BaseNodeDeclaration -> extends Callable<(data: d["prerequisite"]) => unknown, attachmentsOf> { +> extends Callable< + (data: d["prerequisite"], ctx?: TraversalContext) => unknown, + attachmentsOf +> { attachments: UnknownAttachments $: BaseScope constructor(attachments: UnknownAttachments, $: BaseScope) { super( - // pipedFromCtx allows us internally to reuse TraversalContext - // through pipes and keep track of piped paths. It is not exposed (data: any, pipedFromCtx?: TraversalContext) => { if ( !this.includesMorph && @@ -223,19 +224,22 @@ export abstract class BaseNode< // Should be refactored to use transform // https://github.com/arktypeio/arktype/issues/1020 - getIo(kind: "in" | "out"): BaseNode { + getIo(ioKind: "in" | "out"): BaseNode { if (!this.includesMorph) return this as never const ioInner: Record = {} for (const [k, v] of this.innerEntries) { const keySchemaImplementation = this.impl.keys[k] - if (keySchemaImplementation.child) { + if (keySchemaImplementation.reduceIo) + keySchemaImplementation.reduceIo(ioKind, ioInner, v) + else if (keySchemaImplementation.child) { const childValue = v as listable + ioInner[k] = isArray(childValue) ? - childValue.map(child => child[kind]) - : childValue[kind] + childValue.map(child => child[ioKind]) + : childValue[ioKind] } else ioInner[k] = v } diff --git a/ark/schema/package.json b/ark/schema/package.json index ac10e5af4..c21b793d0 100644 --- a/ark/schema/package.json +++ b/ark/schema/package.json @@ -1,6 +1,6 @@ { "name": "@ark/schema", - "version": "0.13.0", + "version": "0.14.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/schema/roots/root.ts b/ark/schema/roots/root.ts index 7f37a39da..d376389ec 100644 --- a/ark/schema/roots/root.ts +++ b/ark/schema/roots/root.ts @@ -9,7 +9,12 @@ import { type array } from "@ark/util" import { throwInvalidOperandError, type Constraint } from "../constraint.ts" -import type { NodeSchema, nodeOfKind, reducibleKindOf } from "../kinds.ts" +import type { + NodeSchema, + mutableNormalizedRootOfKind, + nodeOfKind, + reducibleKindOf +} from "../kinds.ts" import { BaseNode, appendUniqueFlatRefs, @@ -100,6 +105,11 @@ export abstract class BaseRoot< return this } + brand(name: string): this { + if (name === "") return throwParseError(emptyBrandNameMessage) + return this + } + readonly(): this { return this } @@ -389,7 +399,15 @@ export abstract class BaseRoot< in: branch, morphs: [morph] }), - this.$.parseSchema + branches => { + if (!this.hasKind("union")) return this.$.parseSchema(branches[0]) + const schema: mutableNormalizedRootOfKind<"union"> = { + branches + } + if ("default" in this.meta) schema.meta = { default: this.meta.default } + else if (this.meta.optional) schema.meta = { optional: true } + return this.$.parseSchema(schema) + } ) } @@ -573,6 +591,10 @@ export type UndeclaredKeyConfig = { deep?: boolean } +export const emptyBrandNameMessage = `Expected a non-empty brand name after #` + +export type emptyBrandNameMessage = typeof emptyBrandNameMessage + export const exclusivizeRangeSchema = ( schema: schema ): schema => diff --git a/ark/schema/shared/implement.ts b/ark/schema/shared/implement.ts index 639453e6d..6a53eb6cc 100644 --- a/ark/schema/shared/implement.ts +++ b/ark/schema/shared/implement.ts @@ -32,7 +32,7 @@ import type { BaseNormalizedSchema } from "./declare.ts" import type { Disjoint } from "./disjoint.ts" -import { isNode } from "./utils.ts" +import { isNode, type makeRootAndArrayPropertiesMutable } from "./utils.ts" export const basisKinds = ["unit", "proto", "domain"] as const @@ -277,6 +277,11 @@ export type NodeKeyImplementation< preserveUndefined?: true child?: boolean serialize?: (schema: instantiated) => JsonData + reduceIo?: ( + ioKind: "in" | "out", + inner: makeRootAndArrayPropertiesMutable, + value: d["inner"][k] + ) => void parse?: ( schema: Exclude, ctx: NodeParseContext diff --git a/ark/schema/shared/intersections.ts b/ark/schema/shared/intersections.ts index d2323ac9f..70e65beae 100644 --- a/ark/schema/shared/intersections.ts +++ b/ark/schema/shared/intersections.ts @@ -1,10 +1,11 @@ import type { PartialRecord, TypeGuard } from "@ark/util" -import type { nodeOfKind } from "../kinds.ts" +import type { mutableNormalizedRootOfKind, nodeOfKind } from "../kinds.ts" import type { BaseNode } from "../node.ts" import type { Morph } from "../roots/morph.ts" import type { BaseRoot } from "../roots/root.ts" import type { Union } from "../roots/union.ts" import type { BaseScope } from "../scope.ts" +import type { BaseMeta } from "./declare.ts" import { Disjoint } from "./disjoint.ts" import { rootKinds, @@ -130,9 +131,39 @@ const pipeMorphed = ( const viableBranches = results.filter( isNode as TypeGuard ) - return viableBranches.length === 0 ? - Disjoint.init("union", from.branches, to.branches) - : ctx.$.parseSchema(viableBranches) + + if (viableBranches.length === 0) + return Disjoint.init("union", from.branches, to.branches) + + if ( + viableBranches.length < from.branches.length || + !from.branches.every((branch, i) => + branch.in.equals(viableBranches[i].in) + ) + ) + return ctx.$.parseSchema(viableBranches) + + let meta: BaseMeta | undefined + + if ("default" in from.meta) meta = { default: from.meta.default } + else if (from.meta.optional) meta = { optional: true } + + if (viableBranches.length === 1) { + const onlyBranch = viableBranches[0] + if (!meta) return onlyBranch + return ctx.$.node("morph", { + ...onlyBranch.inner, + in: onlyBranch.in.withMeta(meta) + }) + } + + const schema: mutableNormalizedRootOfKind<"union"> = { + branches: viableBranches + } + + if (meta) schema.meta = meta + + return ctx.$.parseSchema(schema) } ) @@ -154,7 +185,7 @@ const _pipeMorphed = ( return ctx.$.node("morph", { morphs, - in: fromIsMorph ? from.in : from + in: from.in }) } diff --git a/ark/schema/structure/optional.ts b/ark/schema/structure/optional.ts index b095067fd..d299cc7d1 100644 --- a/ark/schema/structure/optional.ts +++ b/ark/schema/structure/optional.ts @@ -1,11 +1,20 @@ -import { hasDomain, printable, throwParseError, unset } from "@ark/util" +import { + hasDomain, + omit, + printable, + throwParseError, + unset, + type keySetOf +} from "@ark/util" +import type { Morph } from "../roots/morph.ts" import type { BaseRoot } from "../roots/root.ts" -import type { declareNode } from "../shared/declare.ts" +import type { BaseMeta, declareNode } from "../shared/declare.ts" import { ArkErrors } from "../shared/errors.ts" import { implementNode, type nodeImplementationOf } from "../shared/implement.ts" +import { registeredReference } from "../shared/registry.ts" import { BaseProp, intersectProps, type Prop } from "./prop.ts" export declare namespace Optional { @@ -70,31 +79,80 @@ export class OptionalNode extends BaseProp<"optional"> { } } - expression = `${this.compiledKey}?: ${this.value.expression}${"default" in this.inner ? ` = ${printable(this.inner.default)}` : ""}` -} + get outProp(): Prop.Node { + if (!this.hasDefault()) return this + const { default: defaultValue, ...requiredInner } = this.inner + + requiredInner.value = requiredInner.value.withMeta(meta => + omit(meta, optionalValueMetaKeys) + ) + + return this.cacheGetter( + "outProp", + this.$.node("required", requiredInner, { prereduced: true }) as never + ) + } + + expression: string = `${this.compiledKey}?: ${this.value.expression}${this.hasDefault() ? ` = ${printable(this.inner.default)}` : ""}` + + defaultValueMorphs: Morph[] = this.computeDefaultValueMorphs() + + defaultValueMorphsReference = registeredReference(this.defaultValueMorphs) -// if (parsedValue.meta) { -// if ("default" in parsedValue.meta) { -// return ctx.$.node("optional", { -// key: parsedKey.key, -// value: parsedValue, -// default: parsedValue.meta.default -// }) -// } - -// if (parsedValue.meta.optional) { -// return ctx.$.node("optional", { -// key: parsedKey.key, -// value: parsedValue -// }) -// } -// } + private computeDefaultValueMorphs(): Morph[] { + if (!this.hasDefault()) return [] + + const defaultInput = this.default + + if (typeof defaultInput === "function") { + return [ + // if the value has a morph, pipe context through it + this.value.includesMorph ? + (data, ctx) => { + ctx.path.push(this.key) + this.value((data[this.key] = defaultInput()), ctx) + ctx.path.pop() + return data + } + : data => { + data[this.key] = defaultInput() + return data + } + ] + } + + // non-functional defaults can be safely cached as long as the morph is + // guaranteed to be pure and the output is primitive + const precomputedMorphedDefault = + this.value.includesMorph ? this.value.assert(defaultInput) : defaultInput + + return [ + hasDomain(precomputedMorphedDefault, "object") ? + // the type signature only allows this if the value was morphed + (data, ctx) => { + ctx.path.push(this.key) + this.value((data[this.key] = defaultInput), ctx) + ctx.path.pop() + return data + } + : data => { + data[this.key] = precomputedMorphedDefault + return data + } + ] + } +} export const Optional = { implementation, Node: OptionalNode } +const optionalValueMetaKeys: keySetOf = { + default: 1, + optional: 1 +} + export const assertDefaultValueAssignability = ( node: BaseRoot, value: unknown, diff --git a/ark/schema/structure/prop.ts b/ark/schema/structure/prop.ts index beaa8fde3..7db159efc 100644 --- a/ark/schema/structure/prop.ts +++ b/ark/schema/structure/prop.ts @@ -8,14 +8,13 @@ import { type DeepNodeTransformContext, type FlatRef } from "../node.ts" -import type { Morph } from "../roots/morph.ts" import type { BaseRoot } from "../roots/root.ts" import { compileSerializedValue, type NodeCompiler } from "../shared/compile.ts" import type { BaseNormalizedSchema } from "../shared/declare.ts" import { Disjoint } from "../shared/disjoint.ts" import type { IntersectionContext, RootKind } from "../shared/implement.ts" import { intersectNodes } from "../shared/intersections.ts" -import { $ark, registeredReference } from "../shared/registry.ts" +import { $ark } from "../shared/registry.ts" import type { TraverseAllows, TraverseApply } from "../shared/traversal.ts" import type { Optional } from "./optional.ts" import type { Required } from "./required.ts" @@ -122,33 +121,8 @@ export abstract class BaseProp< return result } - private morphedDefaultFactory: () => unknown = this.getMorphedFactory() - - private defaultValueMorphs: Morph[] = [ - data => { - data[this.key] = this.morphedDefaultFactory() - return data - } - ] - - private defaultValueMorphsReference = registeredReference( - this.defaultValueMorphs - ) - hasDefault(): this is Optional.Node & { default: unknown } { - return "default" in this - } - 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 + return "default" in this.inner } traverseAllows: TraverseAllows = (data, ctx) => { @@ -168,8 +142,7 @@ export abstract class BaseProp< this.value.traverseApply((data as any)[this.key], ctx) ctx.path.pop() } else if (this.hasKind("required")) ctx.error(this.errorContext) - else if (this.hasKind("optional") && this.hasDefault()) - ctx.queueMorphs(this.defaultValueMorphs) + else if (this.hasDefault()) ctx.queueMorphs(this.defaultValueMorphs) } compile(js: NodeCompiler): void { @@ -183,7 +156,7 @@ export abstract class BaseProp< return js.line(`ctx.error(${this.compiledErrorContext})`) else return js.return(false) }) - } else if (js.traversalKind === "Apply" && "default" in this) { + } else if (js.traversalKind === "Apply" && this.hasDefault()) { js.else(() => js.line(`ctx.queueMorphs(${this.defaultValueMorphsReference})`) ) diff --git a/ark/schema/structure/structure.ts b/ark/schema/structure/structure.ts index f89c5f8c7..a0efd07d2 100644 --- a/ark/schema/structure/structure.ts +++ b/ark/schema/structure/structure.ts @@ -49,7 +49,7 @@ import { makeRootAndArrayPropertiesMutable } from "../shared/utils.ts" import type { Index } from "./index.ts" -import { Optional } from "./optional.ts" +import { Optional, type OptionalNode } from "./optional.ts" import type { Prop } from "./prop.ts" import type { Required } from "./required.ts" import type { Sequence } from "./sequence.ts" @@ -128,11 +128,33 @@ const implementation: nodeImplementationOf = keys: { required: { child: true, - parse: constraintKeyParser("required") + parse: constraintKeyParser("required"), + reduceIo: (ioKind, inner, nodes) => { + // ensure we don't overwrite nodes added by optional + inner.required = append( + inner.required, + nodes!.map(node => node[ioKind] as Required.Node) + ) + return + } }, optional: { child: true, - parse: constraintKeyParser("optional") + parse: constraintKeyParser("optional"), + reduceIo: (ioKind, inner, nodes) => { + if (ioKind === "in") { + inner.optional = nodes!.map(node => node.in as OptionalNode) + return + } + + nodes!.forEach( + node => + (inner[node.outProp.kind] = append( + inner[node.outProp.kind], + node.outProp.out as Prop.Node + ) as never) + ) + } }, index: { child: true, diff --git a/ark/type/__tests__/cast.test.ts b/ark/type/__tests__/cast.test.ts index 619a3e393..6f333f10d 100644 --- a/ark/type/__tests__/cast.test.ts +++ b/ark/type/__tests__/cast.test.ts @@ -99,4 +99,21 @@ contextualize(() => { attest(t === from).equals(true) }) }) + + describe("brand", () => { + it("chained", () => { + const t = type("string").brand("foo") + attest(t.t).type.toString.snap('branded<"foo">') + + // no effect at runtime + attest(t.expression).equals("string") + }) + + it("string-embedded", () => { + const t = type("number#cool") + attest(t.t).type.toString.snap('branded<"cool">') + + attest(t.expression).equals("number") + }) + }) }) diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index 89aedee1b..46796d589 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -4,6 +4,7 @@ import { writeNonPrimitiveNonFunctionDefaultValueMessage, writeUnassignableDefaultValueMessage } from "@ark/schema" +import { deepClone } from "@ark/util" import { scope, type } from "arktype" import type { Date } from "arktype/internal/keywords/constructors/Date.ts" import type { @@ -80,8 +81,8 @@ contextualize(() => { "must be a number (was a string)" ) ) - .type.errors.snap( - "Type 'string' is not assignable to type 'number | (() => number)'." + .type.errors( + "Type 'string' is not assignable to type 'DefaultFor'." ) attest(() => // @ts-expect-error @@ -92,9 +93,49 @@ contextualize(() => { "must be a number (was a string)" ) ) - .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'." - ) + .type.errors("Type 'string' is not assignable to type 'number'.") + }) + + it("unions are defaultable", () => { + const t = type("boolean = false") + + attest(t.t).type.toString.snap(` | of> + | of>`) + + attest(t.json).snap({ + branches: [{ unit: false }, { unit: true }], + meta: { default: false } + }) + + const o = type({ + boo: t + }) + + attest(o).type.toString.snap(`Type< + { + boo: + | of> + | of> + }, + {} +>`) + attest(o.json).snap({ + optional: [ + { + default: false, + key: "boo", + value: { + branches: [{ unit: false }, { unit: true }], + meta: { default: false } + } + } + ], + domain: "object" + }) + + attest(o({})).snap({ boo: false }) + attest(o({ boo: true })).snap({ boo: true }) + attest(o({ boo: 5 }).toString()).snap("boo must be boolean (was 5)") }) it("validated default in scope", () => { @@ -156,8 +197,8 @@ contextualize(() => { .throws( writeUnassignableDefaultValueMessage("must be a number (was boolean)") ) - .type.errors( - "'boolean' is not assignable to parameter of type 'number | (() => number)'" + .type.errors.snap( + "Argument of type 'boolean' is not assignable to parameter of type 'DefaultFor'." ) }) @@ -175,8 +216,8 @@ contextualize(() => { .throws( writeUnassignableDefaultValueMessage("must be a number (was boolean)") ) - .type.errors( - "'boolean' is not assignable to parameter of type 'number | (() => number)'" + .type.errors.snap( + "Argument of type 'boolean' is not assignable to parameter of type 'DefaultFor'." ) }) @@ -191,6 +232,13 @@ contextualize(() => { attest<{ bool_value: (In: string.defaultsTo<"off">) => Out }>(processForm.t) + attest<{ + // key should still be distilled as optional even inside a morph + bool_value?: string + }>(processForm.inferIn) + attest<{ + bool_value: boolean + }>(processForm.infer) const out = processForm({}) @@ -222,6 +270,128 @@ contextualize(() => { "bool_value must be a string (was boolean)" ) }) + + it("primitive morph precomputed", () => { + let callCount = 0 + + const toggle = (b: boolean) => { + callCount++ + return !b + } + + const toggleRef = registeredReference(toggle) + + const defaultablePipedBoolean = type("boolean = false").pipe(toggle) + + attest(defaultablePipedBoolean.t).type.toString.snap(`( + In: of> | of> +) => Out`) + attest(defaultablePipedBoolean.json).snap({ + in: [{ unit: false }, { unit: true }], + morphs: [toggleRef], + meta: { default: false } + }) + + const t = type({ + blep: defaultablePipedBoolean + }) + + attest(t.t).type.toString.snap(`{ + blep: ( + In: of> | of> + ) => Out +}`) + + const out = t({}) + + attest(out).snap({ blep: true }) + attest(callCount).equals(1) + + t({}) + attest(callCount).equals(1) + }) + + it("default preserved on pipe to node", () => { + let callCount = 0 + + const toggle = (b: boolean) => { + callCount++ + return !b + } + + const toggleRef = registeredReference(toggle) + + const defaultablePipedBoolean = type("boolean = false") + .pipe(toggle) + .to("boolean") + + attest(defaultablePipedBoolean.t).type.toString.snap(` | (( + In: + | of> + | of> + ) => To) + | (( + In: + | of> + | of> + ) => To)`) + attest(defaultablePipedBoolean.json).snap({ + in: { + branches: [{ unit: false }, { unit: true }], + meta: { default: false } + }, + morphs: [toggleRef, [{ unit: false }, { unit: true }]] + }) + + const t = type({ + blep: defaultablePipedBoolean + }) + + attest(t.t).type.toString.snap(`{ + blep: + | (( + In: + | of> + | of> + ) => To) + | (( + In: + | of> + | of> + ) => To) +}`) + + const out = t({}) + + attest(out).snap({ blep: true }) + attest(callCount).equals(1) + + t({}) + attest(callCount).equals(1) + }) + + it("primitive morphed to object not premorphed", () => { + const toNestedString = type("string") + .default("foo") + .pipe(s => ({ nest: s })) + + const t = type({ foo: toNestedString }) + attest<{ + foo: (In: string.defaultsTo<"foo">) => Out<{ + nest: string + }> + }>(t.t) + + const out = t.assert({}) + + attest(out).snap({ foo: { nest: "foo" } }) + + const originalOut = deepClone(out) + + out.foo.nest = "baz" + + attest(t({})).equals(originalOut) + }) }) describe("string parsing", () => { @@ -293,7 +463,7 @@ contextualize(() => { const expected = type({ key: ["object | null", "=", null] }) attest(expected.t).type.toString.snap( - "{ key: constrain> | null }" + "{ key: of> | null }" ) attest(t) attest(t.json).equals(expected.json) @@ -371,6 +541,17 @@ contextualize(() => { attest(t.t) attest(t.json).equals(expected.json) }) + + it("extracts output as required", () => { + const t = type({ + foo: "string = 'foo'" + }) + + attest<{ foo?: string }>(t.in.infer) + attest<{ foo: string }>(t.out.infer) + attest(t.in.expression).snap('{ foo?: string = "foo" }') + attest(t.out.expression).snap("{ foo: string }") + }) }) describe("works properly with types", () => { @@ -384,8 +565,8 @@ contextualize(() => { bar2: ["unknown.any", "=", () => [true]], baz2: ["unknown.any", "=", () => fn] }) - const v = t.assert({}) - attest(v).snap({ + const out = t.assert({}) + attest(out).snap({ foo1: true, bar1: [true], baz1: fn, @@ -423,7 +604,7 @@ contextualize(() => { ) .throws() .type.errors.snap( - "Type 'boolean' is not assignable to type 'number | (() => number)'." + "Type 'boolean' is not assignable to type 'DefaultFor'." ) attest( // @ts-expect-error @@ -441,23 +622,13 @@ contextualize(() => { .type.errors.snap( "Type 'boolean' is not assignable to type '() => { bar: false; }'." ) - 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[]", "|", "string"], "=", true] }) ) .throws() .type.errors.snap( - "Type 'boolean' is not assignable to type 'string | (() => string | number[])'." + "Type 'boolean' is not assignable to type 'DefaultFor'." ) attest( // @ts-expect-error @@ -465,7 +636,7 @@ contextualize(() => { ) .throws() .type.errors.snap( - "Argument of type 'boolean' is not assignable to parameter of type 'string | (() => string | number[])'." + "Argument of type 'boolean' is not assignable to parameter of type 'DefaultFor'." ) // should not cause "instantiation is excessively deep" attest( @@ -473,8 +644,8 @@ contextualize(() => { () => type("number[]", "|", "string").default(true) ) .throws() - .type.errors( - "not assignable to parameter of type 'string | (() => string | number[])'." + .type.errors.snap( + "Argument of type 'boolean' is not assignable to parameter of type 'DefaultFor'." ) // should not cause "instantiation is excessively deep" attest( @@ -483,7 +654,7 @@ contextualize(() => { ) .throws() .type.errors( - "not assignable to parameter of type 'string | (() => string | number[])'." + "Type 'boolean' is not assignable to type 'string | number[]'." ) }) @@ -497,28 +668,24 @@ contextualize(() => { // @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)'." + .type.errors( + "Type 'boolean' is not assignable to type 'DefaultFor'." ) // @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)'." - ) + .type.errors("Type 'boolean' is not assignable to type '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)'." + "Type 'boolean' is not assignable to type 'DefaultFor'." ) // @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)'." - ) + .type.errors("Type 'boolean' is not assignable to type 'number'.") const f = type({ foo1: "string.numeric.parse = '123'", @@ -588,7 +755,7 @@ contextualize(() => { }) }) - describe("factory functions", () => { + describe("functions", () => { it("works in tuple", () => { const t = type({ foo: ["string", "=", () => "bar"] }) attest(t.assert({ foo: "bar" })).snap({ foo: "bar" }) @@ -640,14 +807,14 @@ contextualize(() => { .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'." + .type.errors( + "Type 'typeof (Anonymous class)' is not assignable to type '() => Function'" ) attest(() => { // @ts-expect-error type({ bar: ["number", "=", (a: number) => a] }) - }).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.errors( + "Type '(a: number) => number' is not assignable to type 'DefaultFor'" ) }) @@ -679,9 +846,7 @@ contextualize(() => { attest(t.assert({})).snap({ foo: [3] }) attest(t.assert({})).snap({ foo: 4 }) }) - }) - describe("works with factories", () => { it("default array", () => { const t = type({ foo: type("number[]").default(() => [1]), @@ -689,8 +854,8 @@ contextualize(() => { .pipe(v => v.map(e => e.toString())) .default(() => [1]) }) - const v1 = t.assert({}), - v2 = t.assert({}) + const v1 = t.assert({}) + const v2 = t.assert({}) attest(v1).snap({ foo: [1], bar: ["1"] }) attest(v1.foo !== v2.foo) }) @@ -731,6 +896,7 @@ contextualize(() => { }) attest(v1.foo !== v2.foo) }) + it("default object is checked", () => { attest(() => { // @ts-expect-error @@ -747,5 +913,31 @@ contextualize(() => { ) ) }) + + it("default allows nested default keys", () => { + const a = type(["string.numeric.parse", "=", "1"]) + + attest(a).type.toString.snap(`Type< + (In: is & Branded<"numeric">>) => To, + {} +>`) + + const defaulted = type({ a }).default(() => ({})) + + attest(defaulted.expression).snap( + '{ a?: (In: string /^(?!^-0$)-?(?:0|[1-9]\\d*)(?:\\.\\d*[1-9])?$/) => Out = "1" }' + ) + attest(defaulted).type.toString.snap(`Type< + of< + { + a: ( + In: is & Branded<"numeric">> + ) => To + }, + Default<{}> + >, + {} +>`) + }) }) }) diff --git a/ark/type/__tests__/get.test.ts b/ark/type/__tests__/get.test.ts index 38b0ac165..b7bf9e92f 100644 --- a/ark/type/__tests__/get.test.ts +++ b/ark/type/__tests__/get.test.ts @@ -1,7 +1,7 @@ import { attest, contextualize } from "@ark/attest" import { writeInvalidKeysMessage, writeNumberIndexMessage } from "@ark/schema" import { keywords, type } from "arktype" -import type { constrain, string } from "arktype/internal/keywords/inference.ts" +import type { of, string } from "arktype/internal/keywords/inference.ts" import type { Matching } from "arktype/internal/keywords/string/string.ts" contextualize(() => { @@ -97,9 +97,7 @@ contextualize(() => { }>(c.infer) attest(c.expression).snap("{ a: 1, b: 1 } | undefined") - const d = t.get( - "foof" as constrain<"foof", Matching<"^f"> & Matching<"f$">> - ) + const d = t.get("foof" as of<"foof", Matching<"^f"> & Matching<"f$">>) // should include { c: 1 } as well but it seems TS can't infer it for now attest< { diff --git a/ark/type/__tests__/imports.test.ts b/ark/type/__tests__/imports.test.ts index cc1af8ccd..fb049c06c 100644 --- a/ark/type/__tests__/imports.test.ts +++ b/ark/type/__tests__/imports.test.ts @@ -168,7 +168,17 @@ contextualize(() => { xdd: "#kekw", "#kekw": "true" }).export() - ).throwsAndHasTypeError(writePrefixedPrivateReferenceMessage("#kekw")) + ).throwsAndHasTypeError(writePrefixedPrivateReferenceMessage("kekw")) + }) + + it("errors on private reference with # in expression", () => { + attest(() => + scope({ + // @ts-expect-error + xdd: "string|#kekw", + "#kekw": "true" + }).export() + ).throwsAndHasTypeError(writePrefixedPrivateReferenceMessage("kekw")) }) it("errors on public and private refrence with same name", () => { diff --git a/ark/type/__tests__/narrow.test.ts b/ark/type/__tests__/narrow.test.ts index 993fb545d..77d547a1e 100644 --- a/ark/type/__tests__/narrow.test.ts +++ b/ark/type/__tests__/narrow.test.ts @@ -5,8 +5,8 @@ import { type } from "arktype" import type { Narrowed, Out, - constrain, number, + of, string } from "arktype/internal/keywords/inference.ts" @@ -72,7 +72,7 @@ contextualize(() => { } ]) - attest>(abEqual.t) + attest>(abEqual.t) attest<{ a: number b: number @@ -164,7 +164,7 @@ contextualize(() => { const A = type("bigint").narrow(predicate).pipe(toString) - attest<(In: constrain) => Out>(A.t) + attest<(In: of) => Out>(A.t) attest(A.in.infer) attest(A.inferIn) attest(A.infer) @@ -244,7 +244,7 @@ contextualize(() => { it("can distill units", () => { const t = type("5").narrow(() => true) - attest>(t.t) + attest>(t.t) attest<5>(t.infer) attest<5>(t.inferIn) diff --git a/ark/type/__tests__/objects/namedKeys.test.ts b/ark/type/__tests__/objects/namedKeys.test.ts index e8686b622..2332d5438 100644 --- a/ark/type/__tests__/objects/namedKeys.test.ts +++ b/ark/type/__tests__/objects/namedKeys.test.ts @@ -1,7 +1,7 @@ import { attest, contextualize } from "@ark/attest" import { registeredReference, writeUnresolvableMessage } from "@ark/schema" import { type } from "arktype" -import type { string } from "arktype/internal/keywords/inference.ts" +import type { Out, string } from "arktype/internal/keywords/inference.ts" contextualize(() => { it("empty", () => { @@ -209,4 +209,32 @@ contextualize(() => { domain: "object" }) }) + + it("morphed", () => { + const processForm = type({ + bool_value: type("string") + .pipe(v => (v === "on" ? true : false)) + .optional() + }) + + attest<{ + bool_value: (In: string.optional) => Out + }>(processForm.t) + attest<{ + // key should still be distilled as optional even inside a morph + bool_value?: string + }>(processForm.inferIn) + attest<{ + // out should also be inferred as optional + bool_value?: boolean + }>(processForm.infer) + + attest(processForm({})).snap({}) + + attest(processForm({ bool_value: "on" })).snap({ bool_value: true }) + + attest(processForm({ bool_value: true }).toString()).snap( + "bool_value must be a string (was boolean)" + ) + }) }) diff --git a/ark/type/__tests__/pipe.test.ts b/ark/type/__tests__/pipe.test.ts index 3d52cd03a..1c281da21 100644 --- a/ark/type/__tests__/pipe.test.ts +++ b/ark/type/__tests__/pipe.test.ts @@ -8,7 +8,7 @@ import { type ArkErrors } from "@ark/schema" import { keywords, scope, type, type Type } from "arktype" -import type { Out, To, constrain } from "arktype/internal/keywords/inference.ts" +import type { Out, To, of } from "arktype/internal/keywords/inference.ts" import type { MoreThan } from "arktype/internal/keywords/number/number.ts" contextualize(() => { @@ -562,7 +562,7 @@ contextualize(() => { b: { a: "1" }, c: "a&b" }).export() - attest<{ a: (In: constrain<1, MoreThan<0>>) => Out }>(types.c.t) + attest<{ a: (In: of<1, MoreThan<0>>) => Out }>(types.c.t) const { serializedMorphs } = types.a.internal.firstReferenceOfKindOrThrow("morph") diff --git a/ark/type/__tests__/realWorld.test.ts b/ark/type/__tests__/realWorld.test.ts index cf228fdf2..75d8356cd 100644 --- a/ark/type/__tests__/realWorld.test.ts +++ b/ark/type/__tests__/realWorld.test.ts @@ -11,8 +11,8 @@ import type { Narrowed, Out, To, - constrain, number, + of, string } from "arktype/internal/keywords/inference.ts" @@ -703,7 +703,7 @@ nospace must be matched by ^\\S*$ (was "One space")`) .pipe(parseBigint) .narrow(validatePositiveBigint) - attest<(In: string | number) => Out>>(Amount.t) + attest<(In: string | number) => Out>>(Amount.t) attest(Amount.json).snap({ in: ["number", "string"], morphs: [morphReference, { predicate: [predicateReference] }] @@ -766,7 +766,7 @@ nospace must be matched by ^\\S*$ (was "One space")`) | { type: "directory" name: is & LessThanLength<255>> - children: constrain< + children: of< ( | { type: "file" diff --git a/ark/type/keywords/constructors/Date.ts b/ark/type/keywords/constructors/Date.ts index 9fa049523..e450ddad3 100644 --- a/ark/type/keywords/constructors/Date.ts +++ b/ark/type/keywords/constructors/Date.ts @@ -1,12 +1,11 @@ import type { Branded, - constrain, constraint, - Constraints, Default, Literal, Narrowed, normalizeLimit, + of, Optional } from "../inference.ts" @@ -27,25 +26,25 @@ export type Before = { } export declare namespace Date { - export type atOrAfter = constrain> + export type atOrAfter = of> - export type after = constrain> + export type after = of> - export type atOrBefore = constrain> + export type atOrBefore = of> - export type before = constrain> + export type before = of> - export type narrowed = constrain + export type narrowed = of - export type optional = constrain + export type optional = of - export type defaultsTo = constrain> + export type defaultsTo = of> - export type branded = constrain> + export type branded = of> - export type literal = constrain> + export type literal = of> - export type is = constrain + export type is = of export type afterSchemaToConstraint = schema extends { exclusive: true } ? After> @@ -55,13 +54,13 @@ export declare namespace Date { schema extends { exclusive: true } ? Before> : AtOrBefore> - export type withConstraint = - constraint extends After ? after - : constraint extends Before ? before - : constraint extends AtOrAfter ? atOrAfter - : constraint extends AtOrBefore ? atOrBefore - : constraint extends Optional ? optional - : constraint extends Default ? defaultsTo - : constraint extends Narrowed ? narrowed + export type applyAttribute = + attribute extends After ? after + : attribute extends Before ? before + : attribute extends AtOrAfter ? atOrAfter + : attribute extends AtOrBefore ? atOrBefore + : attribute extends Optional ? optional + : attribute extends Default ? defaultsTo + : attribute extends Branded ? branded : never } diff --git a/ark/type/keywords/inference.ts b/ark/type/keywords/inference.ts index 3ca85c1b6..cb766e0db 100644 --- a/ark/type/keywords/inference.ts +++ b/ark/type/keywords/inference.ts @@ -11,12 +11,12 @@ import { type anyOrNever, type array, type conform, + type dict, type equals, type Hkt, type intersectArrays, type isSafelyMappable, type leftIfEqual, - type objectKindOf, type Primitive, type show } from "@ark/util" @@ -42,12 +42,18 @@ export type ConstraintSet = Record export type Constraints = Record | { default?: unknown } -export const constrained = noSuggest("arkConstrained") +export interface BaseAttributes { + predicate?: dict<1> + default?: unknown + optional?: 1 +} + +export const attributes = noSuggest("arkAttributes") -export type constrained = typeof constrained +export type attributes = typeof attributes -export type constrain = base & { - [constrained]: constraints +export type of = base & { + [attributes]: [base, attributes] } export type LimitLiteral = number | DateLiteral @@ -101,53 +107,24 @@ export type applyConstraintSchema< t, kind extends Constraint.PrimitiveKind, schema extends NodeSchema -> = applyConstraint> +> = applyAttribute> -export type applyConstraint = +export type applyAttribute = t extends InferredMorph ? - (In: leftIfEqual>) => o - : leftIfEqual> + (In: leftIfEqual>) => o + : leftIfEqual> -type _applyConstraint = +type _applyAttribute = t extends null | undefined ? t - : parseConstraints extends ( - [infer base, infer constraints extends Constraints] - ) ? - [number, base] extends [base, number] ? number.is - : [string, base] extends [base, string] ? - string.is - : [Date, base] extends [base, Date] ? Date.is - : constrain - : [number, t] extends [t, number] ? number.withConstraint - : [string, t] extends [t, string] ? string.withConstraint - : [Date, t] extends [t, Date] ? Date.withConstraint - : constrain> - -export type parseConstraints = - t extends constrain ? - equals extends true ? - [number, constraints] - : equals extends true ? - [string, constraints] - : equals extends true ? - [bigint, constraints] - : equals extends true ? - [symbol, constraints] - : objectKindOf extends infer kind ? - kind extends BuiltinTerminalObjectKind ? - [arkPrototypes.instanceOf, constraints] - : // delegate array constraint distillation to distillArray - kind extends "Array" ? null - : kind extends undefined ? - [ - // if the only key is constrained, the original type could have been {} or unknown, - // so we conservatively allow unknown - keyof base extends constrained ? unknown : Omit, - constraints - ] - : [base, constraints] - : never - : null + : t extends of ? + [number, base] extends [base, number] ? number.is + : [string, base] extends [base, string] ? string.is + : [Date, base] extends [base, Date] ? Date.is + : of + : [number, t] extends [t, number] ? number.applyAttribute + : [string, t] extends [t, string] ? string.applyAttribute + : [Date, t] extends [t, Date] ? Date.applyAttribute + : of> export type normalizePrimitiveConstraintRoot< schema extends NodeSchema @@ -219,28 +196,13 @@ export declare namespace distill { type finalizeDistillation = equals extends true ? t : distilled -export type includesMorphs = - [ - _distill, - _distill - ] extends ( - [ - _distill, - _distill - ] - ) ? - false - : true - type _distill = // ensure optional keys don't prevent extracting defaults t extends undefined ? t : [t] extends [anyOrNever] ? t - : parseConstraints extends ( - [infer base, infer constraints extends Constraints] - ) ? + : t extends of ? opts["branded"] extends true ? - constrain<_distill, constraints> + of<_distill, attributes> : _distill : unknown extends t ? unknown : t extends TerminallyInferredObject | Primitive ? t @@ -288,8 +250,8 @@ type distillUnbrandedIo< > = t extends ( InferredMorph< - constrain, - Out> + of, + Out> > ) ? distillIo< @@ -297,9 +259,9 @@ type distillUnbrandedIo< o extends To ? To : Out, opts > - : t extends InferredMorph> ? + : t extends InferredMorph> ? distillIo - : t extends InferredMorph>> ? + : t extends InferredMorph>> ? distillIo : Out, opts> : distillIo @@ -320,21 +282,27 @@ type inferredOptionalOrDefaultKeyOf = | inferredDefaultKeyOf | inferredOptionalKeyOf +type inOfValueExtends = + [v] extends [anyOrNever] ? false + : [v] extends [t] ? true + : [v] extends [InferredMorph] ? inOfValueExtends + : false + type inferredDefaultKeyOf = keyof o extends infer k ? k extends keyof o ? - [o[k]] extends [anyOrNever] ? never - : o[k] extends InferredDefault ? k - : never + inOfValueExtends extends true ? + k + : never : never : never type inferredOptionalKeyOf = keyof o extends infer k ? k extends keyof o ? - [o[k]] extends [anyOrNever] ? never - : o[k] extends InferredOptional ? k - : never + inOfValueExtends extends true ? + k + : never : never : never @@ -357,14 +325,13 @@ type distillNonArraykeys< distilledArray, opts extends distill.Options > = - keyof originalArray extends keyof distilledArray | constrained ? - distilledArray + keyof originalArray extends keyof distilledArray | attributes ? distilledArray : distilledArray & _distill< { [k in keyof originalArray as k extends ( | keyof distilledArray - | (opts["branded"] extends true ? never : constrained) + | (opts["branded"] extends true ? never : attributes) ) ? never : k]: originalArray[k] @@ -402,14 +369,14 @@ type TerminallyInferredObject = export type inferPredicate = predicate extends (data: any, ...args: any[]) => data is infer narrowed ? - t extends constrain ? - applyConstraintSchema, "predicate", any> + t extends of ? + applyConstraintSchema, "predicate", any> : applyConstraintSchema : applyConstraintSchema export type constrainWithPredicate = - t extends constrain ? - applyConstraintSchema, "predicate", any> + t extends of ? + applyConstraintSchema, "predicate", any> : applyConstraintSchema export type inferPipes = @@ -435,23 +402,22 @@ export type To = ["=>", o, true] export type InferredMorph = (In: i) => o export type Optional = { - optional?: {} + optional: {} } -export type InferredOptional = constrain +export type InferredOptional = of export type Default = { - default?: { value: v } + default: { value: v } } export type DefaultFor = - [t] extends [Primitive] ? (0 extends 1 & t ? Primitive : t) | (() => t) - : | (Primitive extends t ? Primitive - : t extends Primitive ? t - : never) - | (() => t) + | (Primitive extends t ? Primitive + : t extends Primitive ? t + : never) + | (() => t) -export type InferredDefault = constrain> +export type InferredDefault = of> export type termOrType = t | Type @@ -471,21 +437,12 @@ type _inferIntersection = : (In: _inferIntersection) => lOut : r extends InferredMorph ? (In: _inferIntersection) => rOut - : parseConstraints extends ( - [infer lBase, infer lConstraints extends Constraints] - ) ? - parseConstraints extends ( - [infer rBase, infer rConstraints extends Constraints] - ) ? - constrain< - _inferIntersection, - lConstraints & rConstraints - > - : constrain<_inferIntersection, lConstraints> - : parseConstraints extends ( - [infer rBase, infer rConstraints extends Constraints] - ) ? - constrain<_inferIntersection, rConstraints> + : l extends of ? + r extends of ? + of<_inferIntersection, lAttributes & rAttributes> + : of<_inferIntersection, lAttributes> + : r extends of ? + of<_inferIntersection, rAttributes> : [l, r] extends [object, object] ? // adding this intermediate infer result avoids extra instantiations intersectObjects extends infer result ? diff --git a/ark/type/keywords/number/number.ts b/ark/type/keywords/number/number.ts index 8c13642aa..53b0fe529 100644 --- a/ark/type/keywords/number/number.ts +++ b/ark/type/keywords/number/number.ts @@ -1,12 +1,12 @@ import { intrinsic, rootSchema } from "@ark/schema" import type { Module, Submodule } from "../../module.ts" import type { + BaseAttributes, Branded, - constrain, constraint, - Constraints, Default, Narrowed, + of, Optional } from "../inference.ts" import { arkModule } from "../utils.ts" @@ -52,23 +52,32 @@ export type DivisibleBy = { } export declare namespace number { - export type atLeast = constrain> + export type atLeast = of> - export type moreThan = constrain> + export type moreThan = of> - export type atMost = constrain> + export type atMost = of> - export type lessThan = constrain> + export type lessThan = of> - export type divisibleBy = constrain> + export type divisibleBy = of> - export type narrowed = constrain + export type narrowed = of - export type optional = constrain + export type optional = of - export type defaultsTo = constrain> + export type defaultsTo = of> - export type branded = constrain> + export type branded = of> + + interface ownConstraints + extends AtLeast, + MoreThan, + LessThan, + AtMost, + DivisibleBy {} + + export interface Attributes extends BaseAttributes, Partial {} export type NaN = branded<"NaN"> @@ -78,10 +87,7 @@ export declare namespace number { export type safe = branded<"safe"> - export type is = constrain< - number, - constraints - > + export type is = of export type minSchemaToConstraint = schema extends { exclusive: true } ? MoreThan : AtLeast @@ -89,15 +95,15 @@ export declare namespace number { export type maxSchemaToConstraint = schema extends { exclusive: true } ? LessThan : AtMost - export type withConstraint = - constraint extends MoreThan ? moreThan - : constraint extends AtLeast ? atLeast - : constraint extends AtMost ? atMost - : constraint extends LessThan ? lessThan - : constraint extends DivisibleBy ? divisibleBy - : constraint extends Optional ? optional - : constraint extends Default ? defaultsTo - : constraint extends Narrowed ? narrowed + export type applyAttribute = + attribute extends MoreThan ? moreThan + : attribute extends AtLeast ? atLeast + : attribute extends AtMost ? atMost + : attribute extends LessThan ? lessThan + : attribute extends DivisibleBy ? divisibleBy + : attribute extends Optional ? optional + : attribute extends Default ? defaultsTo + : attribute extends Branded ? branded : never export type module = Module diff --git a/ark/type/keywords/string/alpha.ts b/ark/type/keywords/string/alpha.ts index e1c27b635..a2d82cdbf 100644 --- a/ark/type/keywords/string/alpha.ts +++ b/ark/type/keywords/string/alpha.ts @@ -1,8 +1,8 @@ -import type { Branded, constrain } from "../inference.ts" +import type { Branded, of } from "../inference.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type alpha = constrain> + export type alpha = of> } export const alpha = regexStringNode(/^[A-Za-z]*$/, "only letters") diff --git a/ark/type/keywords/string/alphanumeric.ts b/ark/type/keywords/string/alphanumeric.ts index 68321af25..98af1dbd1 100644 --- a/ark/type/keywords/string/alphanumeric.ts +++ b/ark/type/keywords/string/alphanumeric.ts @@ -1,8 +1,8 @@ -import type { Branded, constrain } from "../inference.ts" +import type { Branded, of } from "../inference.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type alphanumeric = constrain> + export type alphanumeric = of> } export const alphanumeric = regexStringNode( diff --git a/ark/type/keywords/string/capitalize.ts b/ark/type/keywords/string/capitalize.ts index 7d56fa85b..96053034c 100644 --- a/ark/type/keywords/string/capitalize.ts +++ b/ark/type/keywords/string/capitalize.ts @@ -1,11 +1,11 @@ import { rootSchema } from "@ark/schema" import type { Module, Submodule } from "../../module.ts" -import type { Branded, constrain, To } from "../inference.ts" +import type { Branded, of, To } from "../inference.ts" import { arkModule } from "../utils.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type capitalized = constrain> + export type capitalized = of> } const preformatted = regexStringNode(/^[A-Z].*$/, "capitalized") diff --git a/ark/type/keywords/string/creditCard.ts b/ark/type/keywords/string/creditCard.ts index ccc3a747e..69dfd6700 100644 --- a/ark/type/keywords/string/creditCard.ts +++ b/ark/type/keywords/string/creditCard.ts @@ -1,5 +1,5 @@ import { rootSchema } from "@ark/schema" -import type { Branded, constrain } from "../inference.ts" +import type { Branded, of } from "../inference.ts" // https://github.com/validatorjs/validator.js/blob/master/src/lib/isLuhnNumber.js export const isLuhnValid = (creditCardInput: string): boolean => { @@ -27,7 +27,7 @@ const creditCardMatcher: RegExp = /^(?:4[0-9]{12}(?:[0-9]{3,6})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12,15}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11}|6[27][0-9]{14}|^(81[0-9]{14,17}))$/ declare namespace string { - export type creditCard = constrain> + export type creditCard = of> } export const creditCard = rootSchema({ diff --git a/ark/type/keywords/string/date.ts b/ark/type/keywords/string/date.ts index ec092c0e9..eb455a1f8 100644 --- a/ark/type/keywords/string/date.ts +++ b/ark/type/keywords/string/date.ts @@ -1,6 +1,11 @@ -import { ArkErrors, intrinsic, rootSchema } from "@ark/schema" +import { + ArkErrors, + intrinsic, + rootSchema, + type TraversalContext +} from "@ark/schema" import type { Module, Submodule } from "../../module.ts" -import type { Branded, To, constrain } from "../inference.ts" +import type { Branded, To, of } from "../inference.ts" import { number } from "../number/number.ts" import { arkModule } from "../utils.ts" import { integer } from "./integer.ts" @@ -153,11 +158,11 @@ const iso = arkModule({ }) declare namespace string { - export type date = constrain> + export type date = of> export namespace date { - export type epoch = constrain> - export type iso = constrain> + export type epoch = of> + export type iso = of> } } @@ -166,7 +171,7 @@ export const stringDate: stringDate.module = arkModule({ parse: rootSchema({ declaredIn: parsableDate, in: "string", - morphs: (s: string, ctx) => { + morphs: (s: string, ctx: TraversalContext) => { const date = new Date(s) if (Number.isNaN(date.valueOf())) return ctx.error("a parsable date") return date diff --git a/ark/type/keywords/string/digits.ts b/ark/type/keywords/string/digits.ts index 5e3f5efbc..e0e4affb9 100644 --- a/ark/type/keywords/string/digits.ts +++ b/ark/type/keywords/string/digits.ts @@ -1,8 +1,8 @@ -import type { Branded, constrain } from "../inference.ts" +import type { Branded, of } from "../inference.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type digits = constrain> + export type digits = of> } export const digits = regexStringNode(/^\d*$/, "only digits 0-9") diff --git a/ark/type/keywords/string/email.ts b/ark/type/keywords/string/email.ts index 8813ce87d..7d67a72c0 100644 --- a/ark/type/keywords/string/email.ts +++ b/ark/type/keywords/string/email.ts @@ -1,8 +1,8 @@ -import type { Branded, constrain } from "../inference.ts" +import type { Branded, of } from "../inference.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type email = constrain> + export type email = of> } export const email = regexStringNode( diff --git a/ark/type/keywords/string/integer.ts b/ark/type/keywords/string/integer.ts index f500ddaa9..ef3f1c6b9 100644 --- a/ark/type/keywords/string/integer.ts +++ b/ark/type/keywords/string/integer.ts @@ -1,13 +1,13 @@ -import { intrinsic, rootSchema } from "@ark/schema" +import { intrinsic, rootSchema, type TraversalContext } from "@ark/schema" import { wellFormedIntegerMatcher } from "@ark/util" import type { Module, Submodule } from "../../module.ts" -import type { Branded, constrain, To } from "../inference.ts" +import type { Branded, of, To } from "../inference.ts" import type { number } from "../number/number.ts" import { arkModule } from "../utils.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type integer = constrain> + export type integer = of> } const root = regexStringNode( @@ -19,7 +19,7 @@ export const integer: stringInteger.module = arkModule({ root, parse: rootSchema({ in: root, - morphs: (s: string, ctx) => { + morphs: (s: string, ctx: TraversalContext) => { const parsed = Number.parseInt(s) return Number.isSafeInteger(parsed) ? parsed : ( ctx.error( diff --git a/ark/type/keywords/string/ip.ts b/ark/type/keywords/string/ip.ts index 0e8ed823d..1a61bab6d 100644 --- a/ark/type/keywords/string/ip.ts +++ b/ark/type/keywords/string/ip.ts @@ -1,5 +1,5 @@ import type { Module, Submodule } from "../../module.ts" -import type { Branded, constrain } from "../inference.ts" +import type { Branded, of } from "../inference.ts" import { arkModule } from "../utils.ts" import { regexStringNode } from "./utils.ts" @@ -24,11 +24,11 @@ const ipv6Matcher = new RegExp( ) declare namespace string { - export type ip = constrain> + export type ip = of> export namespace ip { - export type v4 = constrain> - export type v6 = constrain> + export type v4 = of> + export type v6 = of> } } diff --git a/ark/type/keywords/string/json.ts b/ark/type/keywords/string/json.ts index db6cf2a9d..5b22274b9 100644 --- a/ark/type/keywords/string/json.ts +++ b/ark/type/keywords/string/json.ts @@ -1,10 +1,10 @@ -import { intrinsic, rootSchema } from "@ark/schema" +import { intrinsic, rootSchema, type TraversalContext } from "@ark/schema" import type { Module, Submodule } from "../../module.ts" -import type { Branded, To, constrain } from "../inference.ts" +import type { Branded, To, of } from "../inference.ts" import { arkModule } from "../utils.ts" declare namespace string { - export type json = constrain> + export type json = of> } const jsonStringDescription = "a JSON string" @@ -37,7 +37,7 @@ export const json: stringJson.module = arkModule({ root, parse: rootSchema({ in: "string", - morphs: (s: string, ctx) => { + morphs: (s: string, ctx: TraversalContext) => { if (s.length === 0) { return ctx.error({ code: "predicate", diff --git a/ark/type/keywords/string/lower.ts b/ark/type/keywords/string/lower.ts index 21ccb1d33..47e7e25bd 100644 --- a/ark/type/keywords/string/lower.ts +++ b/ark/type/keywords/string/lower.ts @@ -1,11 +1,11 @@ import { rootSchema } from "@ark/schema" import type { Module, Submodule } from "../../module.ts" -import type { Branded, constrain, To } from "../inference.ts" +import type { Branded, of, To } from "../inference.ts" import { arkModule } from "../utils.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type lowercase = constrain> + export type lowercase = of> } const preformatted = regexStringNode(/^[a-z]*$/, "only lowercase letters") diff --git a/ark/type/keywords/string/normalize.ts b/ark/type/keywords/string/normalize.ts index 4b4a6980c..bf1624740 100644 --- a/ark/type/keywords/string/normalize.ts +++ b/ark/type/keywords/string/normalize.ts @@ -1,17 +1,17 @@ import { rootSchema } from "@ark/schema" import { flatMorph } from "@ark/util" import type { Module, Submodule } from "../../module.ts" -import type { Branded, constrain, To } from "../inference.ts" +import type { Branded, of, To } from "../inference.ts" import { arkModule } from "../utils.ts" declare namespace string { export type normalized = normalized.NFC export namespace normalized { - export type NFC = constrain> - export type NFD = constrain> - export type NFKC = constrain> - export type NFKD = constrain> + export type NFC = of> + export type NFD = of> + export type NFKC = of> + export type NFKD = of> } } diff --git a/ark/type/keywords/string/numeric.ts b/ark/type/keywords/string/numeric.ts index bc554b925..d9d8ca618 100644 --- a/ark/type/keywords/string/numeric.ts +++ b/ark/type/keywords/string/numeric.ts @@ -1,12 +1,12 @@ import { intrinsic, rootSchema } from "@ark/schema" import { wellFormedNumberMatcher } from "@ark/util" import type { Module, Submodule } from "../../module.ts" -import type { Branded, constrain, To } from "../inference.ts" +import type { Branded, of, To } from "../inference.ts" import { arkModule } from "../utils.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type numeric = constrain> + export type numeric = of> } const root = regexStringNode( diff --git a/ark/type/keywords/string/semver.ts b/ark/type/keywords/string/semver.ts index 8d2a8c75c..6779b99f5 100644 --- a/ark/type/keywords/string/semver.ts +++ b/ark/type/keywords/string/semver.ts @@ -1,8 +1,8 @@ -import type { Branded, constrain } from "../inference.ts" +import type { Branded, of } from "../inference.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type semver = constrain> + export type semver = of> } // https://semver.org/ diff --git a/ark/type/keywords/string/string.ts b/ark/type/keywords/string/string.ts index 42b99b743..4b349092c 100644 --- a/ark/type/keywords/string/string.ts +++ b/ark/type/keywords/string/string.ts @@ -4,15 +4,14 @@ import type { AtLeastLength, AtMostLength, Branded, - Constraints, Default, ExactlyLength, LessThanLength, MoreThanLength, Narrowed, Optional, - constrain, - constraint + constraint, + of } from "../inference.ts" import { arkModule } from "../utils.ts" import { alpha } from "./alpha.ts" @@ -61,41 +60,38 @@ export type Matching = { } export declare namespace string { - export type atLeastLength = constrain> + export type atLeastLength = of> - export type moreThanLength = constrain> + export type moreThanLength = of> - export type atMostLength = constrain> + export type atMostLength = of> - export type lessThanLength = constrain> + export type lessThanLength = of> - export type exactlyLength = constrain> + export type exactlyLength = of> - export type matching = constrain> + export type matching = of> - export type narrowed = constrain + export type narrowed = of - export type optional = constrain + export type optional = of - export type defaultsTo = constrain> + export type defaultsTo = of> - export type branded = constrain> + export type branded = of> - export type is = constrain< - string, - constraints - > + export type is = of - export type withConstraint = - constraint extends ExactlyLength ? exactlyLength - : constraint extends MoreThanLength ? moreThanLength - : constraint extends AtLeastLength ? atLeastLength - : constraint extends AtMostLength ? atMostLength - : constraint extends LessThanLength ? lessThanLength - : constraint extends Matching ? matching - : constraint extends Optional ? optional - : constraint extends Default ? defaultsTo - : constraint extends Narrowed ? narrowed + export type applyAttribute = + attribute extends ExactlyLength ? exactlyLength + : attribute extends MoreThanLength ? moreThanLength + : attribute extends AtLeastLength ? atLeastLength + : attribute extends AtMostLength ? atMostLength + : attribute extends LessThanLength ? lessThanLength + : attribute extends Matching ? matching + : attribute extends Optional ? optional + : attribute extends Default ? defaultsTo + : attribute extends Branded ? branded : never export type module = Module diff --git a/ark/type/keywords/string/trim.ts b/ark/type/keywords/string/trim.ts index b40dc8dac..ae9095156 100644 --- a/ark/type/keywords/string/trim.ts +++ b/ark/type/keywords/string/trim.ts @@ -1,11 +1,11 @@ import { rootSchema } from "@ark/schema" import type { Module, Submodule } from "../../module.ts" -import type { Branded, constrain, To } from "../inference.ts" +import type { Branded, of, To } from "../inference.ts" import { arkModule } from "../utils.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type trimmed = constrain> + export type trimmed = of> } const preformatted = regexStringNode( diff --git a/ark/type/keywords/string/upper.ts b/ark/type/keywords/string/upper.ts index eb4c3592e..cfd3d181a 100644 --- a/ark/type/keywords/string/upper.ts +++ b/ark/type/keywords/string/upper.ts @@ -1,11 +1,11 @@ import { rootSchema } from "@ark/schema" import type { Module, Submodule } from "../../module.ts" -import type { Branded, constrain, To } from "../inference.ts" +import type { Branded, of, To } from "../inference.ts" import { arkModule } from "../utils.ts" import { regexStringNode } from "./utils.ts" declare namespace string { - export type uppercase = constrain> + export type uppercase = of> } const preformatted = regexStringNode(/^[A-Z]*$/, "only uppercase letters") diff --git a/ark/type/keywords/string/url.ts b/ark/type/keywords/string/url.ts index aa79a79f8..a9bd5b342 100644 --- a/ark/type/keywords/string/url.ts +++ b/ark/type/keywords/string/url.ts @@ -1,10 +1,10 @@ -import { rootSchema } from "@ark/schema" +import { rootSchema, type TraversalContext } from "@ark/schema" import type { Module, Submodule } from "../../module.ts" -import type { Branded, constrain, To } from "../inference.ts" +import type { Branded, of, To } from "../inference.ts" import { arkModule } from "../utils.ts" declare namespace string { - export type url = constrain> + export type url = of> } const isParsableUrl = (s: string) => { @@ -31,7 +31,7 @@ export const url: url.module = arkModule({ parse: rootSchema({ declaredIn: root as never, in: "string", - morphs: (s: string, ctx) => { + morphs: (s: string, ctx: TraversalContext) => { try { return new URL(s) } catch { diff --git a/ark/type/keywords/string/uuid.ts b/ark/type/keywords/string/uuid.ts index a458cebd3..f62bc0bd9 100644 --- a/ark/type/keywords/string/uuid.ts +++ b/ark/type/keywords/string/uuid.ts @@ -1,5 +1,5 @@ import type { Module, Submodule } from "../../module.ts" -import type { Branded, constrain } from "../inference.ts" +import type { Branded, of } from "../inference.ts" import { arkModule } from "../utils.ts" import { regexStringNode } from "./utils.ts" @@ -47,17 +47,17 @@ export const uuid = arkModule({ }) declare namespace string { - export type uuid = constrain> + export type uuid = of> export namespace uuid { - export type v1 = constrain> - export type v2 = constrain> - export type v3 = constrain> - export type v4 = constrain> - export type v5 = constrain> - export type v6 = constrain> - export type v7 = constrain> - export type v8 = constrain> + export type v1 = of> + export type v2 = of> + export type v3 = of> + export type v4 = of> + export type v5 = of> + export type v6 = of> + export type v7 = of> + export type v8 = of> } } diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index b20871d32..4d0afd825 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -18,7 +18,8 @@ import type { } from "@ark/util" import type { ArkAmbient } from "../config.ts" import type { - applyConstraint, + applyAttribute, + Branded, Default, DefaultFor, distill, @@ -79,6 +80,14 @@ interface Type as(...args: validateChainedAsArgs): instantiateType + brand< + const name extends string, + r = applyAttribute> extends infer r ? instantiateType + : never + >( + name: name + ): r + get in(): instantiateType get out(): instantiateType @@ -138,14 +147,14 @@ interface Type // inferring r into an alias in the return doesn't // work the way it does for the other methods here - optional>(): instantiateType + optional>(): instantiateType default< const value extends this["inferIn"], - r = applyConstraint> + r = applyAttribute> >( value: DefaultFor - ): NoInfer> + ): NoInfer> extends infer result ? result : never // deprecate Function methods so they are deprioritized as suggestions @@ -181,7 +190,7 @@ interface Type interface ChainedPipeSignature { >, r = instantiateType, $>>( a: a - ): NoInfer + ): NoInfer extends infer result ? result : never < a extends Morph>, b extends Morph>, @@ -189,7 +198,7 @@ interface ChainedPipeSignature { >( a: a, b: b - ): NoInfer + ): NoInfer extends infer result ? result : never < a extends Morph>, b extends Morph>, @@ -199,7 +208,7 @@ interface ChainedPipeSignature { a: a, b: b, c: c - ): NoInfer + ): NoInfer extends infer result ? result : never < a extends Morph>, b extends Morph>, @@ -211,7 +220,7 @@ interface ChainedPipeSignature { b: b, c: c, d: d - ): NoInfer + ): NoInfer extends infer result ? result : never < a extends Morph>, b extends Morph>, @@ -225,7 +234,7 @@ interface ChainedPipeSignature { c: c, d: d, e: e - ): NoInfer + ): NoInfer extends infer result ? result : never < a extends Morph>, b extends Morph>, @@ -241,7 +250,7 @@ interface ChainedPipeSignature { d: d, e: e, f: f - ): NoInfer + ): NoInfer extends infer result ? result : never < a extends Morph>, b extends Morph>, @@ -259,7 +268,7 @@ interface ChainedPipeSignature { e: e, f: f, g: g - ): NoInfer + ): NoInfer extends infer result ? result : never } export interface ChainedPipes extends ChainedPipeSignature { diff --git a/ark/type/methods/object.ts b/ark/type/methods/object.ts index ad2ec1c6b..7ea99ce85 100644 --- a/ark/type/methods/object.ts +++ b/ark/type/methods/object.ts @@ -20,11 +20,10 @@ import type { toArkKey } from "@ark/util" import type { - applyConstraint, - constrain, + applyAttribute, Default, - Optional, - parseConstraints + of, + Optional } from "../keywords/inference.ts" import type { type } from "../keywords/keywords.ts" import type { ArrayType } from "./array.ts" @@ -105,23 +104,21 @@ type typePropOf = : never type typeProp = - parseConstraints extends ( - [infer base, infer constraints extends Default | Optional] - ) ? - constraints extends Default ? + t extends of ? + attributes extends Default ? DefaultedTypeProp< k & Key, - keyof constraints extends keyof Default ? base - : constrain>, + keyof attributes extends keyof Default ? base + : of>, defaultValue, $ > - : constraints extends Optional ? + : attributes extends Optional ? BaseTypeProp< "optional", k & Key, - keyof constraints extends keyof Optional ? base - : constrain>, + keyof attributes extends keyof Optional ? base + : of>, $ > : never @@ -197,7 +194,7 @@ type fromTypeProps> = show< [prop in props[number] as Extract< applyHomomorphicOptionality, { kind: "optional"; default: unknown } - >["key"]]: applyConstraint< + >["key"]]: applyAttribute< prop["value"][inferred], Default > diff --git a/ark/type/package.json b/ark/type/package.json index cec3b2650..d84ab1d25 100644 --- a/ark/type/package.json +++ b/ark/type/package.json @@ -1,7 +1,7 @@ { "name": "arktype", "description": "TypeScript's 1:1 validator, optimized from editor to runtime", - "version": "2.0.0-rc.11", + "version": "2.0.0-rc.12", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/type/parser/ast/infer.ts b/ark/type/parser/ast/infer.ts index a35387d5f..95a924a60 100644 --- a/ark/type/parser/ast/infer.ts +++ b/ark/type/parser/ast/infer.ts @@ -2,10 +2,11 @@ import type { GenericAst } from "@ark/schema" import type { Hkt, arkKeyOf, array } from "@ark/util" import type { Date } from "../../keywords/constructors/Date.ts" import type { + Branded, Default, LimitLiteral, Optional, - applyConstraint, + applyAttribute, applyConstraintSchema, distill, inferIntersection, @@ -15,6 +16,7 @@ import type { type } from "../../keywords/keywords.ts" import type { UnparsedScope } from "../../scope.ts" import type { inferDefinition } from "../definition.ts" import type { Comparator, MinComparator } from "../reduce/shared.ts" +import type { Scanner } from "../shift/scanner.ts" export type inferAstRoot = ast extends array ? inferExpression : never @@ -82,8 +84,10 @@ export type inferExpression = // unscoped type.infer is safe since the default value is always a literal // as of TS5.6, inlining defaultValue causes a bunch of extra types and instantiations type.infer extends infer defaultValue ? - applyConstraint, Default> + applyAttribute, Default> : never + : ast[1] extends "#" ? + applyAttribute, Branded> : ast[1] extends Comparator ? ast[0] extends LimitLiteral ? brandBound, ast[1], ast[0]> @@ -99,7 +103,7 @@ export type inferExpression = ast[2] & number > : ast[1] extends "?" ? - applyConstraint, Optional> + applyAttribute, Optional> : ast[0] extends "keyof" ? arkKeyOf> : never : never @@ -140,25 +144,13 @@ export type PrefixExpression< operand = unknown > = [operator, operand] -export type PostfixOperator = "[]" | "?" - export type PostfixExpression< - operator extends PostfixOperator = PostfixOperator, + operator extends Scanner.PostfixToken = Scanner.PostfixToken, operand = unknown > = readonly [operand, operator] -export type InfixOperator = - | "|" - | "&" - | Comparator - | "%" - | ":" - | "=>" - | "@" - | "=" - export type InfixExpression< - operator extends InfixOperator = InfixOperator, + operator extends Scanner.InfixToken = Scanner.InfixToken, l = unknown, r = unknown > = [l, operator, r] diff --git a/ark/type/parser/ast/validate.ts b/ark/type/parser/ast/validate.ts index 7bd171754..d25618f61 100644 --- a/ark/type/parser/ast/validate.ts +++ b/ark/type/parser/ast/validate.ts @@ -9,7 +9,6 @@ import type { anyOrNever, array, BigintLiteral, - charsAfterFirst, Completion, ErrorMessage, NumberLiteral, @@ -39,8 +38,8 @@ export type validateAst = ast extends ErrorMessage ? ast : ast extends InferredAst ? validateInferredAst : ast extends DefAst ? - ast[2] extends PrivateDeclaration ? - ErrorMessage> + ast[2] extends PrivateDeclaration ? + ErrorMessage> : undefined : ast extends PostfixExpression ? operator extends "[]" ? validateAst @@ -51,7 +50,8 @@ export type validateAst = : operator extends Comparator ? validateRange : operator extends "%" ? validateDivisor : operator extends "=" ? validateDefault - : never + : operator extends "#" ? validateAst + : ErrorMessage>> : ast extends ["keyof", infer operand] ? validateKeyof : ast extends GenericInstantiationAst ? validateGenericArgs @@ -82,17 +82,13 @@ type validateGenericArgs< > : undefined -export const writePrefixedPrivateReferenceMessage = < - def extends PrivateDeclaration ->( - def: def -): writePrefixedPrivateReferenceMessage => - `Private type references should not include '#'. Use '${def.slice(1) as charsAfterFirst}' instead.` +export const writePrefixedPrivateReferenceMessage = ( + name: name +): writePrefixedPrivateReferenceMessage => + `Private type references should not include '#'. Use '${name}' instead.` -export type writePrefixedPrivateReferenceMessage< - def extends PrivateDeclaration -> = - `Private type references should not include '#'. Use '${charsAfterFirst}' instead.` +export type writePrefixedPrivateReferenceMessage = + `Private type references should not include '#'. Use '${name}' instead.` type validateInferredAst = def extends NumberLiteral ? @@ -104,8 +100,8 @@ type validateInferredAst = ErrorMessage> : undefined : [inferred] extends [anyOrNever] ? undefined - : def extends PrivateDeclaration ? - ErrorMessage> + : def extends PrivateDeclaration ? + ErrorMessage> : // these problems would've been caught during a fullStringParse, but it's most // efficient to check for them here in case the string was naively parsed inferred extends Generic ? diff --git a/ark/type/parser/objectLiteral.ts b/ark/type/parser/objectLiteral.ts index d5dfd656d..be45f6abd 100644 --- a/ark/type/parser/objectLiteral.ts +++ b/ark/type/parser/objectLiteral.ts @@ -27,7 +27,7 @@ import { type mutable, type show } from "@ark/util" -import type { constrain } from "../keywords/inference.ts" +import type { of } from "../keywords/inference.ts" import type { astToString } from "./ast/utils.ts" import type { validateString } from "./ast/validate.ts" import type { inferDefinition, validateDefinition } from "./definition.ts" @@ -118,7 +118,7 @@ export type validateObjectLiteral = { validateString extends ErrorMessage ? // add a nominal type here to avoid allowing the error message as input ErrorType - : inferDefinition extends Key | constrain ? + : inferDefinition extends Key | of ? // if the indexDef is syntactically and semantically valid, // move on to the validating the value definition validateDefinition diff --git a/ark/type/parser/reduce/dynamic.ts b/ark/type/parser/reduce/dynamic.ts index e1bb901e6..62c3978f3 100644 --- a/ark/type/parser/reduce/dynamic.ts +++ b/ark/type/parser/reduce/dynamic.ts @@ -6,7 +6,6 @@ import { throwParseError } from "@ark/util" import type { LimitLiteral } from "../../keywords/inference.ts" -import type { InfixOperator } from "../ast/infer.ts" import { parseOperand } from "../shift/operand/operand.ts" import { parseOperator } from "../shift/operator/operator.ts" import type { Scanner } from "../shift/scanner.ts" @@ -189,7 +188,7 @@ export class DynamicState { previousOperator(): | MinComparator | StringifiablePrefixOperator - | InfixOperator + | Scanner.InfixToken | undefined { return ( this.branches.leftBound?.comparator ?? diff --git a/ark/type/parser/shift/operand/unenclosed.ts b/ark/type/parser/shift/operand/unenclosed.ts index f78b25e56..29c9e418e 100644 --- a/ark/type/parser/shift/operand/unenclosed.ts +++ b/ark/type/parser/shift/operand/unenclosed.ts @@ -4,7 +4,6 @@ import { type BaseRoot, type GenericAst, type GenericRoot, - type PrivateDeclaration, type arkKind, type genericParamNames, type resolvableReferenceIn, @@ -104,9 +103,12 @@ const unenclosedToNode = (s: DynamicState, token: string): BaseRoot => maybeParseReference(s, token) ?? maybeParseUnenclosedLiteral(s, token) ?? s.error( - token === "" ? writeMissingOperandMessage(s) - : token[0] === "#" ? - writePrefixedPrivateReferenceMessage(token as PrivateDeclaration) + token === "" ? + s.scanner.lookahead === "#" ? + writePrefixedPrivateReferenceMessage( + s.shiftedByOne().scanner.shiftUntilNextTerminator() + ) + : writeMissingOperandMessage(s) : writeUnresolvableMessage(token) ) @@ -229,7 +231,13 @@ export type unresolvableState< args, submodulePath extends string[] > = - validReferenceFromToken extends ( + [token, s["unscanned"]] extends ["", Scanner.shift<"#", infer unscanned>] ? + Scanner.shiftUntilNextTerminator extends ( + Scanner.shiftResult + ) ? + state.error> + : never + : validReferenceFromToken extends ( never ) ? state.error< diff --git a/ark/type/parser/shift/operator/brand.ts b/ark/type/parser/shift/operator/brand.ts new file mode 100644 index 000000000..5f894b932 --- /dev/null +++ b/ark/type/parser/shift/operator/brand.ts @@ -0,0 +1,19 @@ +import type { emptyBrandNameMessage } from "@ark/schema" +import type { DynamicStateWithRoot } from "../../reduce/dynamic.ts" +import type { StaticState, state } from "../../reduce/static.ts" +import type { Scanner } from "../scanner.ts" + +export const parseBrand = (s: DynamicStateWithRoot): void => { + s.scanner.shiftUntilNonWhitespace() + const brandName = s.scanner.shiftUntilNextTerminator() + s.root = s.root.brand(brandName) +} + +export type parseBrand = + Scanner.shiftUntilNextTerminator> extends ( + Scanner.shiftResult<`${infer brandName}`, infer nextUnscanned> + ) ? + brandName extends "" ? + state.error + : state.setRoot + : never diff --git a/ark/type/parser/shift/operator/operator.ts b/ark/type/parser/shift/operator/operator.ts index 6d1ac5783..caf34da91 100644 --- a/ark/type/parser/shift/operator/operator.ts +++ b/ark/type/parser/shift/operator/operator.ts @@ -7,6 +7,7 @@ import { parseBound, type ComparatorStartChar } from "./bounds.ts" +import { parseBrand } from "./brand.ts" import { parseDivisor } from "./divisor.ts" export const parseOperator = (s: DynamicStateWithRoot): void => { @@ -23,6 +24,7 @@ export const parseOperator = (s: DynamicStateWithRoot): void => { s.finalize(lookahead) : isKeyOf(lookahead, comparatorStartChars) ? parseBound(s, lookahead) : lookahead === "%" ? parseDivisor(s) + : lookahead === "#" ? parseBrand(s) : lookahead in whiteSpaceTokens ? parseOperator(s) : s.error(writeUnexpectedCharacterMessage(lookahead)) ) @@ -44,6 +46,7 @@ export type parseOperator = : lookahead extends ComparatorStartChar ? parseBound : lookahead extends "%" ? parseDivisor + : lookahead extends "#" ? parseBrand : lookahead extends WhiteSpaceToken ? parseOperator, $, args> : state.error> diff --git a/ark/type/parser/shift/scanner.ts b/ark/type/parser/shift/scanner.ts index 4f378b2db..3aa5c1194 100644 --- a/ark/type/parser/shift/scanner.ts +++ b/ark/type/parser/shift/scanner.ts @@ -102,6 +102,7 @@ export class Scanner { ",": true, ":": true, "?": true, + "#": true, ...whiteSpaceTokens } as const @@ -173,9 +174,18 @@ export declare namespace Scanner { export type FinalizingLookahead = keyof typeof Scanner.finalizingLookaheads - export type InfixToken = Comparator | "|" | "&" | "%" | ":" | "=>" - - export type PostfixToken = "[]" + export type InfixToken = + | Comparator + | "|" + | "&" + | "%" + | ":" + | "=>" + | "#" + | "@" + | "=" + + export type PostfixToken = "[]" | "?" export type OperatorToken = InfixToken | PostfixToken diff --git a/ark/type/parser/tuple.ts b/ark/type/parser/tuple.ts index c64b4decd..0c51df8d4 100644 --- a/ark/type/parser/tuple.ts +++ b/ark/type/parser/tuple.ts @@ -27,7 +27,7 @@ import { type show } from "@ark/util" import type { - applyConstraint, + applyAttribute, Default, DefaultFor, distill, @@ -39,9 +39,10 @@ import type { Out } from "../keywords/inference.ts" import type { type } from "../keywords/keywords.ts" -import type { InfixOperator, PostfixExpression } from "./ast/infer.ts" +import type { PostfixExpression } from "./ast/infer.ts" import type { inferDefinition, validateDefinition } from "./definition.ts" import { writeMissingRightOperandMessage } from "./shift/operand/unenclosed.ts" +import type { Scanner } from "./shift/scanner.ts" import type { BaseCompletions } from "./string.ts" export const parseTuple = (def: array, ctx: BaseParseContext): BaseRoot => @@ -176,7 +177,7 @@ const maybeParseTupleExpression = ( // It is *extremely* important we use readonly any time we check a tuple against // something like this. Not doing so will always cause the check to fail, since // def is declared as a const parameter. -type InfixExpression = readonly [unknown, InfixOperator, ...unknown[]] +type InfixExpression = readonly [unknown, Scanner.InfixToken, ...unknown[]] export type validateTuple = def extends IndexZeroExpression ? validatePrefixExpression @@ -377,9 +378,9 @@ export type inferTupleExpression = : def[1] extends "=>" ? parseMorph : def[1] extends "@" ? inferDefinition : def[1] extends "=" ? - applyConstraint, Default> + applyAttribute, Default> : def[1] extends "?" ? - applyConstraint, Optional> + applyAttribute, Optional> : def extends readonly ["===", ...infer values] ? values[number] : def extends ( readonly ["instanceof", ...infer constructors extends Constructor[]] diff --git a/ark/util/package.json b/ark/util/package.json index 5655ffdb4..dc7f6100e 100644 --- a/ark/util/package.json +++ b/ark/util/package.json @@ -1,6 +1,6 @@ { "name": "@ark/util", - "version": "0.13.0", + "version": "0.14.0", "author": { "name": "David Blass", "email": "david@arktype.io", diff --git a/ark/util/registry.ts b/ark/util/registry.ts index b4018b177..e0641612d 100644 --- a/ark/util/registry.ts +++ b/ark/util/registry.ts @@ -7,7 +7,7 @@ import { FileConstructor, objectKindOf } from "./objectKinds.ts" // recent node versions (https://nodejs.org/api/esm.html#json-modules). // For now, we assert this matches the package.json version via a unit test. -export const arkUtilVersion = "0.13.0" +export const arkUtilVersion = "0.14.0" export const initialRegistryContents = { version: arkUtilVersion,