diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4384a00f00..80d940443d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,4 @@ github: [arktypeio] drips: - ethereum: - ownedBy: "0xD5c5Fe5DF95adf8DA1Ae640fCAE8f72795657fa5" + ethereum: + ownedBy: "0xD5c5Fe5DF95adf8DA1Ae640fCAE8f72795657fa5" diff --git a/README.md b/README.md index db0ba7283d..289cb00c0a 100644 --- a/README.md +++ b/README.md @@ -146,15 +146,12 @@ In the meantime, check out the examples here and use the type hints you get to l ArkType can easily be used with tRPC via the `assert` prop: ```ts -... -t.procedure - .input( - type({ - name: "string", - "age?": "number" - }).assert - ) -... +t.procedure.input( + type({ + name: "string", + "age?": "number" + }).assert +) ``` ## How? diff --git a/ark/attest/__tests__/unwrap.test.ts b/ark/attest/__tests__/unwrap.test.ts index 679056f54e..a873eb7599 100644 --- a/ark/attest/__tests__/unwrap.test.ts +++ b/ark/attest/__tests__/unwrap.test.ts @@ -1,15 +1,27 @@ import { attest, contextualize } from "@ark/attest" +import type { Completions } from "@ark/attest/internal/cache/writeAssertionCache.js" +import type { autocomplete } from "@ark/util" contextualize(() => { it("unwraps unversioned", () => { - attest(attest({ foo: "bar" }).unwrap()).equals({ + const unwrapped = attest({ foo: "bar" }).unwrap() + attest<{ foo: string }>(unwrapped).equals({ foo: "bar" }) }) it("unwraps serialized", () => { - attest( - attest({ foo: Symbol("unwrappedSymbol") }).unwrap({ serialize: true }) - ).snap({ foo: "Symbol(unwrappedSymbol)" }) + const unwrapped = attest({ foo: Symbol("unwrappedSymbol") }).unwrap({ + serialize: true + }) + attest(unwrapped).snap({ foo: "Symbol(unwrappedSymbol)" }) + }) + + it("unwraps completions", () => { + const unwrapped = attest({ foo: "b" } satisfies { + foo: autocomplete<"bar"> + }).completions.unwrap() + + attest(unwrapped).snap({ b: ["bar"] }) }) }) diff --git a/ark/attest/assert/chainableAssertions.ts b/ark/attest/assert/chainableAssertions.ts index 20c4cf56c7..0f6e0a0516 100644 --- a/ark/attest/assert/chainableAssertions.ts +++ b/ark/attest/assert/chainableAssertions.ts @@ -155,7 +155,10 @@ export class ChainableAssertions implements AssertionRecord { return this } - return Object.assign(inline, { toFile }) + return Object.assign(inline, { + toFile, + unwrap: this.unwrap.bind(this) + }) } private immediateOrChained() { @@ -314,18 +317,26 @@ type snapProperty = { id: string, options?: ExternalSnapshotOptions ) => nextAssertions + unwrap: Unwrapper } +export type Unwrapper = (opts?: UnwrapOptions) => expected + export type comparableValueAssertion = { snap: snapProperty equals: (value: expected) => nextAssertions instanceOf: (constructor: Constructor) => nextAssertions is: (value: expected) => nextAssertions - completions: (value?: Completions) => void + completions: CompletionsSnap satisfies: (def: type.validate) => nextAssertions // This can be used to assert values without type constraints unknown: Omit, "unknown"> - unwrap: (opts?: UnwrapOptions) => unknown + unwrap: Unwrapper +} + +export interface CompletionsSnap { + (value?: Completions): void + unwrap: Unwrapper } export type TypeAssertionsRoot = { @@ -335,7 +346,7 @@ export type TypeAssertionsRoot = { export type TypeAssertionProps = { toString: valueFromTypeAssertion errors: valueFromTypeAssertion - completions: (value?: Completions) => void + completions: CompletionsSnap } export type ExternalSnapshotOptions = { diff --git a/ark/attest/package.json b/ark/attest/package.json index b43c669f92..a14d1c4637 100644 --- a/ark/attest/package.json +++ b/ark/attest/package.json @@ -1,6 +1,6 @@ { "name": "@ark/attest", - "version": "0.27.0", + "version": "0.28.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/docs/README.md b/ark/docs/README.md index f9d358dc86..b3a63033f9 100644 --- a/ark/docs/README.md +++ b/ark/docs/README.md @@ -3,3 +3,113 @@ Source code for ArkType's docs at [arktype.io](https://arktype.io) Built with [Starlight](https://starlight.astro.build/) + +# Definitions + +- Primitives + + - string + - keywords + - Autogenerate from JSDoc + - literals + - patterns + - lengths + - number + - keywords + - Autogenerate from JSDoc + - literals + - ranges + - divisors + - more + - bigint + - boolean + - symbol + - null + - undefined + +- Objects + + - properties + - required + - optional + - defaultable + - index + - undeclared + - more + - merge + - keyof + - get + - map + - arrays + - lengths + - tuples + - prefix + - optional + - variadic + - postfix + - dates + - keywords + - Autogenerate from JSDoc + - literals + - ranges + - instanceOf + - keywords + - Autogenerate from JSDoc + +- Expressions + + - intersection + - union + - brand + - narrow + - morph + - more + - unit + - enumerated + - meta + - cast + - parenthetical + - this + +# Other stuff + +- Types (how you can use existing types) + + - Top-level type invocation + + - Autogenerate from JSDoc + + - define + - raw + +- Configuration + + - errors + - clone + - onUndeclaredKey + - jitless + +- Scopes (advanced) + + - syntax + - modules + - visibility + - submodules + - thunks + +- Generics (advanced) + + - keywords + - Autogenerate from JSDoc + - syntax + - hkt (advanced++) + +- Integrations + + - Standard Schema + - tRPC + - react-hook-form + - hono + +- FAQ +- About the project diff --git a/ark/docs/astro.config.js b/ark/docs/astro.config.js index c2fbff2a07..ce04a02910 100644 --- a/ark/docs/astro.config.js +++ b/ark/docs/astro.config.js @@ -9,7 +9,9 @@ import { shikiConfig } from "./src/components/shiki.config.js" export default defineConfig({ site: "https://arktype.io", redirects: { - "/discord": "https://discord.gg/xEzdc3fJQC" + "/discord": "https://discord.gg/xEzdc3fJQC", + "/primitives/string": "/string", + "/primitives/number": "/number" }, // cannot configure out dir to out to match other packges since dist is hard // coded into: https://github.com/withastro/action/blob/main/action.yml @@ -44,115 +46,306 @@ export default defineConfig({ } }, { - label: "Keywords", - collapsed: true, + label: "Primitives", items: [ { - label: "TypeScript", - link: "/keywords#typescript" + label: "string", + collapsed: true, + items: [ + { + label: "keywords", + link: "/primitives#string/keywords" + }, + { label: "literals", link: "/string#literals" }, + { + label: "patterns", + link: "/primitives#string/patterns" + }, + { + label: "lengths", + link: "/primitives#string/lengths" + } + ] }, { - label: "Subtype", - link: "/keywords#subtype" + label: "number", + collapsed: true, + items: [ + { + label: "keywords", + link: "/primitives#number/keywords" + }, + { label: "literals", link: "/primitives#number/literals" }, + { + label: "ranges", + link: "/primitives#number/ranges" + }, + { + label: "divisors", + link: "/primitives#number/divisors" + } + ] + }, + { + label: "bigint", + link: "/primitives#bigint" + }, + { + label: "boolean", + link: "/primitives#boolean" + }, + { + label: "symbol", + link: "/primitives#symbol" + }, + { + label: "null", + link: "/primitives#null" + }, + { + label: "undefined", + link: "/primitives#undefined" + } + ] + }, + { + label: "Objects", + items: [ + { + label: "properties", + collapsed: true, + items: [ + { + label: "required", + link: "/objects#properties/required" + }, + { + label: "optional", + link: "/objects#properties/optional" + }, + { + label: "defaultable", + link: "/objects#properties/defaultable" + }, + { + label: "index", + link: "/objects#properties/index" + }, + { + label: "undeclared", + link: "/objects#properties/undeclared" + }, + { + label: "merge", + link: "/objects#properties/merge" + }, + { + label: "keyof", + link: "/objects#properties/keyof" + }, + { + label: "get", + link: "/objects#properties/get" + } + ] + }, + { + label: "arrays", + collapsed: true, + items: [{ label: "lengths", link: "/objects#arrays/lengths" }] + }, + { + label: "tuples", + collapsed: true, + items: [ + { + label: "prefix", + link: "/objects#tuples/prefix" + }, + { + label: "optional", + link: "/objects#tuples/optional" + }, + { + label: "variadic", + link: "/objects#tuples/variadic" + }, + { + label: "postfix", + link: "/objects#tuples/postfix" + } + ] + }, + { + label: "dates", + collapsed: true, + items: [ + { label: "keywords", link: "/objects#dates/keywords" }, + { label: "literals", link: "/objects#dates/literals" }, + { label: "ranges", link: "/objects#dates/ranges" } + ] + }, + { + label: "instanceof", + link: "/objects#instanceof" + } + ] + }, + { + label: "Expressions", + items: [ + { + label: "intersection", + link: "/expressions#intersection" + }, + { + label: "union", + link: "/expressions#union" + }, + { + label: "brand", + link: "/expressions#brand" + }, + { + label: "narrow", + link: "/expressions#narrow" + }, + { + label: "morph", + link: "/expressions#morph" + }, + { + label: "unit", + link: "/expressions#unit" + }, + { + label: "enumerated", + link: "/expressions#enumerated" + }, + { + label: "meta", + link: "/expressions#meta" + }, + { + label: "cast", + link: "/expressions#cast" + }, + { + label: "parenthetical", + link: "/expressions#parenthetical" }, { label: "this", - link: "/keywords#this" + link: "/expressions#this" } ] }, { - label: "Literals", - collapsed: true, + label: "Types", items: [ - { label: "String", link: "/literals#string" }, - { label: "Number", link: "/literals#number" }, - { label: "Bigint", link: "/literals#bigint" }, - { label: "Date", link: "/literals#date" }, - { label: "Regex", link: "/literals#regex" } + { + label: "properties", + link: "/types#properties" + }, + { label: "utilities", link: "/types#utilities" } ] }, { - label: "Objects", + label: "Configuration", items: [ { - label: "Required Properties", - link: "/objects#required-properties" + label: "errors", + link: "/configuration#errors" }, { - label: "Optional Properties", - link: "/objects#optional-properties" + label: "clone", + link: "/configuration#clone" }, { - label: "Defaultable Properties", - link: "/objects#defaultable-properties" + label: "onUndeclaredKey", + link: "/configuration#onundeclaredkey" }, { - label: "Index Signatures", - link: "/objects#index-signatures" + label: "jitless", + link: "/configuration#jitless" } ] }, { - label: "Tuples", - collapsed: true, + label: "Scopes", + badge: "advanced", items: [ { - label: "Prefix Elements", - link: "/tuples#prefix-elements" + label: "modules", + link: "/scopes#modules" }, { - label: "Optional Elements", - link: "/tuples#optional-elements" + label: "visibility", + link: "/scopes#visibility" }, { - label: "Variadic Elements", - link: "/tuples#variadic-elements" + label: "submodules", + link: "/scopes#submodules" }, { - label: "Postfix Elements", - link: "/tuples#postfix-elements" + label: "thunks", + link: "/scopes#thunks" } ] }, { - label: "Expressions", + label: "Generics", + badge: "advanced", items: [ { - label: "Array", - link: "/expressions#array" + label: "keywords", + link: "/generics#keywords" }, { - label: "Divisibility", - link: "/expressions#divisibility" + label: "syntax", + link: "/generics#syntax" }, { - label: "Equality", - link: "/expressions#equality" + label: "hkt", + link: "/generics#hkt", + badge: "advanced++" + } + ] + }, + { + label: "Integrations", + translations: {}, + items: [ + { + label: "Standard Schema", + link: "/integrations#standard-schema" }, { - label: "Parenthetical", - link: "/expressions#parenthetical" + label: "tRPC", + link: "/integrations#trpc" }, { - label: "instanceof", - link: "/expressions#instanceof" + label: "react-hook-form", + link: "/integrations#react-hook-form" }, { - label: "Union", - link: "/expressions#union" + label: "hono", + link: "/integrations#hono" } ] }, { - label: "Reference", - autogenerate: { - directory: "reference" - } + label: "FAQ", + link: "/faq" + }, + { + label: "About the project", + link: "/about" } ], components: { - Head: "./src/components/Head.astro" + Head: "./src/components/Head.astro", + Sidebar: "./src/components/Sidebar.astro" }, customCss: ["@shikijs/twoslash/style-rich.css", "./src/styles.css"], expressiveCode: false, diff --git a/ark/docs/package.json b/ark/docs/package.json index a63400c228..f46ed1bc7e 100644 --- a/ark/docs/package.json +++ b/ark/docs/package.json @@ -16,24 +16,24 @@ "@ark/util": "workspace:*", "@astrojs/check": "0.9.4", "@astrojs/react": "3.6.2", - "@astrojs/starlight": "0.28.3", - "@astrojs/ts-plugin": "1.10.3", - "@shikijs/transformers": "1.22.0", - "@shikijs/twoslash": "1.22.0", + "@astrojs/starlight": "0.29.0", + "@astrojs/ts-plugin": "1.10.4", + "@shikijs/transformers": "1.22.2", + "@shikijs/twoslash": "1.22.2", "arkdark": "workspace:*", "arktype": "workspace:*", - "astro": "4.16.6", + "astro": "4.16.12", "astro-og-canvas": "0.5.4", "canvaskit-wasm": "0.39.1", - "framer-motion": "11.11.9", + "framer-motion": "11.11.17", "react": "18.3.1", "react-dom": "18.3.1", "sharp": "0.33.5", - "shiki": "1.22.0", + "shiki": "1.22.2", "twoslash": "0.2.12" }, "devDependencies": { - "@types/react": "18.3.11", + "@types/react": "18.3.12", "@types/react-dom": "18.3.1", "typescript": "catalog:" } diff --git a/ark/docs/src/components/Sidebar.astro b/ark/docs/src/components/Sidebar.astro new file mode 100644 index 0000000000..8661fd3720 --- /dev/null +++ b/ark/docs/src/components/Sidebar.astro @@ -0,0 +1,22 @@ +--- +import type { Props } from "@astrojs/starlight/props" +import BuiltinSidebar from "@astrojs/starlight/components/Sidebar.astro" +import { experimental_AstroContainer } from "astro/container" + +const container = await experimental_AstroContainer.create() +const html = await container.renderToString(BuiltinSidebar, Astro) + +const onToggleSrc = `if(!this.open) { +const firstChildLink = this.querySelector('a').href +const lastDelimiterIndex = Math.max(firstChildLink.lastIndexOf('/'), firstChildLink.lastIndexOf('#')) +const groupLink = firstChildLink.slice(0, lastDelimiterIndex) +window.location.href = groupLink +}`.replaceAll("\n", ";") + +const newHtml = html.replaceAll( + /
/g, + (match, p1) => `
` +) +--- + + diff --git a/ark/docs/src/content/docs/about.mdx b/ark/docs/src/content/docs/about.mdx new file mode 100644 index 0000000000..b01ed16e8c --- /dev/null +++ b/ark/docs/src/content/docs/about.mdx @@ -0,0 +1,5 @@ +--- +title: About the project +--- + +🚧 Coming soon ™️🚧 diff --git a/ark/docs/src/content/docs/configuration.mdx b/ark/docs/src/content/docs/configuration.mdx new file mode 100644 index 0000000000..1e8f67d2ba --- /dev/null +++ b/ark/docs/src/content/docs/configuration.mdx @@ -0,0 +1,19 @@ +--- +title: Configuration +--- + +### Errors + +🚧 Coming soon ™️🚧 + +### Clone + +🚧 Coming soon ™️🚧 + +### onUndeclaredKey + +🚧 Coming soon ™️🚧 + +### jitless + +🚧 Coming soon ™️🚧 diff --git a/ark/docs/src/content/docs/expressions.mdx b/ark/docs/src/content/docs/expressions.mdx index 613a8cf03e..6c579c296f 100644 --- a/ark/docs/src/content/docs/expressions.mdx +++ b/ark/docs/src/content/docs/expressions.mdx @@ -5,14 +5,20 @@ title: Expressions import { Tabs } from "@astrojs/starlight/components" import SyntaxTab from "../../components/SyntaxTab.astro" -### Array +### Intersection + +🚧 Coming soon ™️🚧 + +### Union + +All unions are automatically discriminated to optimize check time and error message clarity. ```ts -const arrays = type({ - key: "string[]" +const unions = type({ + key: "string | number" }) ``` @@ -21,8 +27,8 @@ const arrays = type({ ```ts -const arrays = type({ - key: type.string.array() +const unions = type({ + key: type.string.or(type.number) }) ``` @@ -31,8 +37,8 @@ const arrays = type({ ```ts -const arrays = type({ - key: [{ name: "string" }, "[]"] +const unions = type({ + key: ["string", "|", { name: "string" }] }) ``` @@ -41,8 +47,8 @@ const arrays = type({ ```ts -const arrays = type({ - key: type({ name: "string" }, "[]") +const unions = type({ + key: type("string", "|", { name: "string" }) }) ``` @@ -50,36 +56,21 @@ const arrays = type({ -### Divisibility - -Constrain a `number` to a multiple of the specified integer. - - - - -```ts -const evens = type({ - key: "number%2" -}) -``` +### Brand - +🚧 Coming soon ™️🚧 - +### Narrow -```ts -const evens = type({ - key: type.number.divisibleBy(2) -}) -``` +🚧 Coming soon ™️🚧 - +### Morph - +🚧 Coming soon ™️🚧 -### Equality +### Unit -While embedded [literal syntax](/literals) is usually ideal for defining exact primitive values, the `===` operator, `type.unit` and `type.enumerated` can be helpful for defining a non-serializable reference like a `symbol` or for deriving a type from a pre-existing list of allowed values. +While embedded [literal syntax](/literals) is usually ideal for defining exact primitive values, `===` and `type.unit` can be helpful for referencing a non-serialiazable value like a `symbol` from your type. @@ -89,8 +80,6 @@ While embedded [literal syntax](/literals) is usually ideal for defining exact p const mySymbol = Symbol() const exactValue = type.unit(mySymbol) - -const exactValueFromSet = type.enumerated(1337, true, null) ``` @@ -101,8 +90,6 @@ const exactValueFromSet = type.enumerated(1337, true, null) const mySymbol = Symbol() const exactValue = type(["===", mySymbol]) - -const exactValueFromSet = type(["===", 1337, true, null]) ``` @@ -113,37 +100,24 @@ const exactValueFromSet = type(["===", 1337, true, null]) const mySymbol = Symbol() const exactValue = type("===", mySymbol) - -const exactValueFromSet = type("===", 1337, true, null) ``` -### Parenthetical - -By default, ArkType's operators follow the same precedence as TypeScript's. Also like in TypeScript, this can be overridden by wrapping an expression in parentheses. - -```ts -// hover to see the distinction! -const groups = type({ - stringOrArrayOfNumbers: "string | number[]", - arrayOfStringsOrNumbers: "(string | number)[]" -}) -``` - -### instanceof +### Enumerated -Most builtin instance types like `Array` and `Date` are available directly as keywords, but `instanceof` can be useful for constraining a type to one of your own classes. +`type.enumerated` defines a type based on a list of allowed values. It is semantically equivalent to `type.unit` if provided a single value. + ```ts -class MyClass {} +const mySymbol = Symbol() -const instances = type.instanceOf(MyClass) +const exactValueFromSet = type.enumerated(1337, true, mySymbol) ``` @@ -151,11 +125,9 @@ const instances = type.instanceOf(MyClass) ```ts -class MyClass {} +const mySymbol = Symbol() -const instances = type({ - key: ["instanceof", MyClass] -}) +const exactValueFromSet = type(["===", 1337, true, mySymbol]) ``` @@ -163,60 +135,64 @@ const instances = type({ ```ts -class MyClass {} +const mySymbol = Symbol() -const instances = type({ - key: type("instanceof", MyClass) -}) +const exactValueFromSet = type("===", 1337, true, mySymbol) ``` -### Union +### Meta -All unions are automatically discriminated to optimize check time and error message clarity. +🚧 Coming soon ™️🚧 - - +### Cast -```ts -const unions = type({ - key: "string | number" -}) -``` +🚧 Coming soon ™️🚧 - +### Parenthetical - +By default, ArkType's operators follow the same precedence as TypeScript's. Also like in TypeScript, this can be overridden by wrapping an expression in parentheses. ```ts -const unions = type({ - key: type.string.or(type.number) +// hover to see the distinction! +const groups = type({ + stringOrArrayOfNumbers: "string | number[]", + arrayOfStringsOrNumbers: "(string | number)[]" }) ``` - +### this - +`this` is a special keyword that can be used to create a recursive type referencing the root of the current definition. ```ts -const unions = type({ - key: ["string", "|", { name: "string" }] -}) -``` +const fetchGift = async () => null +// ---cut--- +const disappointingGift = type({ label: "string", "box?": "this" }) +const myGift = disappointingGift.assert(await fetchGift()) - +// hover me +const chainable = myGift.box?.box?.label +``` - +Unlike its TypeScript counterpart, ArkType's `this` is not limited to interfaces. It can also be used from within a tuple expression. ```ts -const unions = type({ - key: type("string", "|", { name: "string" }) -}) +// boxes now expects an array of our gift object +const disappointingGifts = type({ label: "string", boxes: "this" }, "[]") ``` - +Referencing `this` from within a scope will result in a ParseError. For similar behavior within a scoped definition, just reference the alias by name: - +```ts +const types = scope({ + disappointingGift: { + label: "string", + // Resolves correctly to the root of the current type + "box?": "disappointingGift" + } +}).export() +``` diff --git a/ark/docs/src/content/docs/reference/faq.md b/ark/docs/src/content/docs/faq.mdx similarity index 100% rename from ark/docs/src/content/docs/reference/faq.md rename to ark/docs/src/content/docs/faq.mdx diff --git a/ark/docs/src/content/docs/reference/generics.md b/ark/docs/src/content/docs/generics.mdx similarity index 88% rename from ark/docs/src/content/docs/reference/generics.md rename to ark/docs/src/content/docs/generics.mdx index 07f1f3215a..a355d869fa 100644 --- a/ark/docs/src/content/docs/reference/generics.md +++ b/ark/docs/src/content/docs/generics.mdx @@ -4,11 +4,45 @@ sidebar: order: 3 --- -Native generic syntax is finally available! 🎉 +### Keywords -Here are some examples of how this powerful feature can be used: +Record is now available as a builtin keyword. + +```ts +import { type } from "arktype" + +const stringRecord = type("Record") +``` + +In addition to `Record`, the following generics from TS are now available in ArkType: + +- **Pick** +- **Omit** +- **Extract** +- **Exclude** + +These can be instantiated in one of three ways: + +```ts +import { type, type Type } from "arktype" -#### Standalone Type Syntax +const createBox = (of: type.Any) => + type({ + box: of + }) + +const boxType = createBox(type("string")) + +// @ts-expect-error +const badBox = createBox(type("number")) + +console.log(boxType({ box: 5 }).toString()) +console.log(boxType({ box: "foo" })) +``` + +### Syntax + +#### Definition ```ts import { type } from "arktype" @@ -48,26 +82,7 @@ const types = scope({ const out = types.bitBox({ box: 0 }) ``` -#### Builtins - -Record is now available as a builtin keyword. - -```ts -import { type } from "arktype" - -const stringRecord = type("Record") -``` - -In addition to `Record`, the following generics from TS are now available in ArkType: - -- **Pick** -- **Omit** -- **Extract** -- **Exclude** - -These can be instantiated in one of three ways: - -### Syntactic Definition +#### Invocation ```ts import { type } from "arktype" @@ -75,7 +90,7 @@ import { type } from "arktype" const one = type("Extract<0 | 1, 1>") ``` -### Chained Definition +##### Chained ```ts import { type } from "arktype" @@ -90,7 +105,7 @@ const user = type({ const basicUser = user.pick("name", "age") ``` -### Invoked Definition +#### Invoked ```ts import { type } from "arktype" @@ -98,27 +113,7 @@ import { type } from "arktype" const unfalse = type.keywords.Exclude("boolean", "false") ``` - - -### Generic HKTs +### HKT Our new generics have been built using a new method for integrating arbitrary external types as native ArkType generics! This opens up tons of possibilities for external integrations that would otherwise not be possible. As a preview, here's what the implementation of `Partial` looks like internally: diff --git a/ark/docs/src/content/docs/integrations.mdx b/ark/docs/src/content/docs/integrations.mdx new file mode 100644 index 0000000000..deed05c149 --- /dev/null +++ b/ark/docs/src/content/docs/integrations.mdx @@ -0,0 +1,31 @@ +--- +title: Integrations +--- + +### Standard Schema + +ArkType is proud to support and co-author the new [Standard Schema](https://github.com/standard-schema/standard-schema) API with [Valibot](https://github.com/fabian-hiller/valibot) and [Zod](https://github.com/colinhacks/zod). + +Standard Schema allows you and your dependencies to integrate library-agnostic validation logic. If you're building or maintaining a library with a peer dependency on ArkType and/or other validation libraries, we'd recommend consuming it through Standard Schema's API if possible so that your users can choose the solution that best suits their needs! + +### tRPC + +ArkType can easily be used with tRPC via the `assert` prop: + +```ts +// @noErrors +t.procedure.input( + type({ + name: "string", + "age?": "number" + }).assert +) +``` + +### react-hook-form + +🚧 Coming soon ™️🚧 + +### hono + +🚧 Coming soon ™️🚧 diff --git a/ark/docs/src/content/docs/intro/setup.mdx b/ark/docs/src/content/docs/intro/setup.mdx index a15aede4ae..417a959648 100644 --- a/ark/docs/src/content/docs/intro/setup.mdx +++ b/ark/docs/src/content/docs/intro/setup.mdx @@ -43,7 +43,11 @@ To take advantage of all of ArkType's autocomplete capabilities, you'll need to // allow autocomplete for ArkType expressions like "string | num" "editor.quickSuggestions": { "strings": "on" -} +}, +// prioritize ArkType's "type" for autoimports +"typescript.preferences.autoImportSpecifierExcludeRegexes": [ + "^(node:)?os$" +], ``` ### Extension (optional) diff --git a/ark/docs/src/content/docs/keywords.mdx b/ark/docs/src/content/docs/keywords.mdx index 6040a0c0fa..092726ddf9 100644 --- a/ark/docs/src/content/docs/keywords.mdx +++ b/ark/docs/src/content/docs/keywords.mdx @@ -71,36 +71,3 @@ All builtin keywords and modules are available in `type.keywords`. - -### this - -`this` is a special keyword that can be used to create a recursive type referencing the root of the current definition. - -```ts -const fetchGift = async () => null -// ---cut--- -const disappointingGift = type({ label: "string", "box?": "this" }) -const myGift = disappointingGift.assert(await fetchGift()) - -// hover me -const chainable = myGift.box?.box?.label -``` - -Unlike its TypeScript counterpart, ArkType's `this` is not limited to interfaces. It can also be used from within a tuple expression. - -```ts -// boxes now expects an array of our gift object -const disappointingGifts = type({ label: "string", boxes: "this" }, "[]") -``` - -Referencing `this` from within a scope will result in a ParseError. For similar behavior within a scoped definition, just reference the alias by name: - -```ts -const types = scope({ - disappointingGift: { - label: "string", - // Resolves correctly to the root of the current type - "box?": "disappointingGift" - } -}).export() -``` diff --git a/ark/docs/src/content/docs/literals.mdx b/ark/docs/src/content/docs/literals.mdx deleted file mode 100644 index 3a613b566b..0000000000 --- a/ark/docs/src/content/docs/literals.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Literals ---- - -import { Tabs } from "@astrojs/starlight/components" -import SyntaxTab from "../../components/SyntaxTab.astro" - -Literals offer string-embedded syntax for specifying exact values, paralleling TypeScript where applicable. - -### String - -```ts -const literals = type({ - singleQuoted: "'typescript'", - doubleQuoted: '"arktype"' -}) -``` - -### Number - -```ts -const literals = type({ - number: "1337" -}) -``` - -### Bigint - -```ts -const literals = type({ - bigint: "1337n" -}) -``` - -### Date - -Date literals represent a Date instance with an exact value. - -They're primarily useful in ranges. - -```ts -const literals = type({ - singleQuoted: "d'01-01-1970'", - doubleQuoted: 'd"01-01-1970"' -}) -``` - -### Regex - -Regex literals specify an unanchored regular expression that an input string must match. - -They can either be string-embedded or refer directly to a `RegExp` instance. - -```ts -const literals = type({ - stringEmbedded: "/^a.*z$/", - regexLiteral: /^a.*z$/ -}) -``` diff --git a/ark/docs/src/content/docs/objects.mdx b/ark/docs/src/content/docs/objects.mdx index bbe070cdc0..2df8e4ae9b 100644 --- a/ark/docs/src/content/docs/objects.mdx +++ b/ark/docs/src/content/docs/objects.mdx @@ -5,9 +5,12 @@ title: Objects import { Tabs } from "@astrojs/starlight/components" import SyntaxTab from "../../components/SyntaxTab.astro" +## properties + Objects definitions can include any combination of required, optional, defaultable named properties and index signatures. -### Required Properties + +##### required @@ -44,7 +47,8 @@ const myObject = type({ -### Optional Properties + +##### optional @@ -101,6 +105,29 @@ const myObject = type({ +:::caution[Optional properties cannot be present with the value `undefined` unless specified] + +In TypeScript, there is a setting called `exactOptionalPropertyTypes` that can be set to `true` to enforce the distinction between properties that are missing and properties that are present with the value `undefined`. + +ArkType mirrors this behavior by default, so if you want to allow `undefined`, you'll need to add it to your value's definition. If you're interested in a builtin configuration option for this setting, we'd love feedback or contributions on [this issue](https://github.com/arktypeio/arktype/issues/1191). + +
+ See an example + +```ts +const myObj = type({ + "key?": "number" +}) + +// valid data +const validResult = myObj({}) + +// Error: key must be a number (was undefined) +const errorResult = myObj({ key: undefined }) +``` + +
+ :::caution[Optional and default only work within objects!] Adding a `optional` or `default` to a `Type` doesn't alter its standalone behavior. @@ -126,7 +153,8 @@ objectWithOptionalKey.allows({}) // true Prefer the key-embedded syntax (`"optionalKey?":`) where possible. ::: -### Defaultable Properties +
+##### defaultable @@ -171,7 +199,8 @@ const myObject = type({ -### Index Signatures + +##### index @@ -188,3 +217,301 @@ const myObject = type({ + + +##### undeclared + +🚧 Coming soon ™️🚧 + + +##### merge + +🚧 Coming soon ™️🚧 + + +##### keyof + +🚧 Coming soon ™️🚧 + + +##### get + +🚧 Coming soon ™️🚧 + +## arrays + + + + +```ts +const arrays = type({ + key: "string[]" +}) +``` + + + + + +```ts +const arrays = type({ + key: type.string.array() +}) +``` + + + + + +```ts +const arrays = type({ + key: [{ name: "string" }, "[]"] +}) +``` + + + + + +```ts +const arrays = type({ + key: type({ name: "string" }, "[]") +}) +``` + + + + + + +##### lengths + +🚧 Coming soon ™️🚧 + +## tuples + +Like objects, tuples are structures whose values are nested definitions. Like TypeScript, ArkType supports prefix, optional, variadic, and postfix elements, with the same restrictions about combining them. + + +##### prefix + + + + +```ts +const myTuple = type([ + "string", + // Object definitions can be nested in tuples- and vice versa! + { + coordinates: ["number", "number"] + } +]) +``` + + + + + +```ts +const myTuple = type([ + type.string, + // Object definitions can be nested in tuples- and vice versa! + { + coordinates: [type.number, type.number] + } +]) +``` + + + + + + +##### optional + +Tuples can include any number of optional elements following its prefix elements. + +Like in TypeScript, optional elements are mutually exclusive with postfix elements. + + + + +```ts +const myTuple = type(["string", "boolean?", "number?"]) +``` + + + + + +```ts +const myTuple = type([ + type.string, + type.boolean.optional(), + type.number.optional() +]) +``` + + + + + +```ts +const myTuple = type([ + "string", + [ + { + name: "string" + }, + "?" + ] +]) +``` + + + + + +```ts +const myTuple = type([ + "string", + type( + { + name: "string" + }, + "?" + ) +]) +``` + + + + + + +##### variadic + +Like in TypeScript, variadic elements allow zero or more consecutive values of a given type and may occur at most once in a tuple. + +They are specified with a `"..."` operator preceding an array element. + + + + +```ts +// allows a string followed by zero or more numbers +const myTuple = type(["string", "...", "number[]"]) +``` + + + + + +```ts +// allows a string followed by zero or more numbers +const myTuple = type([type.string, "...", type.number.array()]) +``` + + + + + + +##### postfix + +Postfix elements are required elements following a variadic element. + +They are mutually exclusive with optional elements. + + + + +```ts +// allows zero or more numbers followed by a boolean, then a string +const myTuple = type(["...", "number[]", "boolean", "string"]) +``` + + + + + +```ts +// allows zero or more numbers followed by a boolean, then a string +const myTuple = type(["...", type.number.array(), type.boolean, type.string]) +``` + + + + + +## dates + + +##### keywords + +🚧 Coming soon ™️🚧 + + +##### literals + +Date literals represent a Date instance with an exact value. + +They're primarily useful in ranges. + +```ts +const literals = type({ + singleQuoted: "d'01-01-1970'", + doubleQuoted: 'd"01-01-1970"' +}) +``` + + +##### ranges + +🚧 Coming soon ™️🚧 + +## instanceof + +Most builtin instance types like `Array` and `Date` are available directly as keywords, but `instanceof` can be useful for constraining a type to one of your own classes. + + + + +```ts +class MyClass {} + +const instances = type.instanceOf(MyClass) +``` + + + + + +```ts +class MyClass {} + +const instances = type({ + key: ["instanceof", MyClass] +}) +``` + + + + + +```ts +class MyClass {} + +const instances = type({ + key: type("instanceof", MyClass) +}) +``` + + + + + + +##### keywords + +🚧 Coming soon ™️🚧 diff --git a/ark/docs/src/content/docs/primitives.mdx b/ark/docs/src/content/docs/primitives.mdx new file mode 100644 index 0000000000..732a3325d7 --- /dev/null +++ b/ark/docs/src/content/docs/primitives.mdx @@ -0,0 +1,300 @@ +--- +title: Primitives +--- + +import { Tabs } from "@astrojs/starlight/components" +import SyntaxTab from "../../components/SyntaxTab.astro" + +## string + + +##### keywords + +🚧 Coming soon ™️🚧 + + +##### literals + +```ts +const literals = type({ + singleQuoted: "'typescript'", + doubleQuoted: '"arktype"' +}) +``` + + +##### patterns + +Regex literals specify an unanchored regular expression that an input string must match. + +They can either be string-embedded or refer directly to a `RegExp` instance. + +```ts +const literals = type({ + stringEmbedded: "/^a.*z$/", + regexLiteral: /^a.*z$/ +}) +``` + + +##### lengths + +🚧 Coming soon ™️🚧 + +## number + + +##### keywords + +🚧 Coming soon ™️🚧 + + +##### literals + +```ts +const literals = type({ + number: "1337" +}) +``` + + +##### ranges + +🚧 Coming soon ™️🚧 + + +##### divisors + +Constrain a `number` to a multiple of the specified integer. + + + + +```ts +const evens = type({ + key: "number % 2" +}) +``` + + + + + +```ts +const evens = type({ + key: type.number.divisibleBy(2) +}) +``` + + + + + +## bigint + +To allow any `bigint` value, use the `"bigint"` keyword. + + + + +```ts +const bigints = type({ + foo: "bigint" +}) +``` + + + + + +```ts +const symbols = type({ + foo: type.bigint +}) +``` + + + + + + +##### literals + +To require an exact `bigint` value in your type, you can use add the suffix `n` to a string-embedded [number literal](/primitives#number/literals) to make it a `bigint`. + +```ts +const literals = type({ + bigint: "1337n" +}) +``` + +You may also use a [unit expression](/expressions#unit) to define `bigint` literals. + +## symbol + +To allow any `symbol` value, use the `"symbol"` keyword. + + + + +```ts +const symbols = type({ + key: "symbol" +}) +``` + + + + + +```ts +const symbols = type({ + key: type.symbol +}) +``` + + + + + +To reference a specific symbol in your definition, use a [unit expression](/expressions#unit). + +No special syntax is required to define symbolic properties like `{ [mySymbol]: "string" }`. For more information and examples of how to combine symbolic keys with other syntax like optionality, see [properties](/objects#properties). + +## boolean + +To allow `true` or `false`, use the `"boolean"` keyword. + + + + +```ts +const booleans = type({ + key: "boolean" +}) +``` + + + + + +```ts +const booleans = type({ + key: type.boolean +}) +``` + + + + + + +##### literals + +To require a specific boolean value, use the corresponding literal. + + + + +```ts +const booleans = type({ + a: "true", + b: "false", + // equivalent to the "boolean" keyword + c: "true | false" +}) +``` + + + + + +```ts +const booleans = type({ + a: type.keywords.true, + b: type.keywords.false, + // equivalent to the "boolean" keyword + c: type.keywords.true.or(type.keywords.false) +}) +``` + + + + + +## null + +The `"null"` keyword can be used to allow the exact value `null`, generally as part of a [union](/expressions#union). + + + + +```ts +const nullable = type({ + requiredKey: "number | null" +}) +``` + + + + + +```ts +const nullable = type({ + requiredKey: type.number.or(type.null) +}) +``` + + + + + +## undefined + +The `"undefined"` keyword can be used to allow the exact value `undefined`, generally as part of a [union](/expressions#union). + + + + +```ts +const myObj = type({ + requiredKey: "number | undefined" +}) +``` + + + + + +```ts +const myObj = type({ + requiredKey: type.number.or(type.undefined) +}) +``` + + + + + +:::caution[Allowing `undefined` as a value does not make the key optional!] + +In TypeScript, a required property that allows `undefined` must still be present for the type to be satisfied. + +The same is true in ArkType. + +
+ See an example + +```ts +const myObj = type({ + key: "number | undefined" +}) + +// valid data +const validResult = myObj({ key: undefined }) + +// Error: name must be a number or undefined (was missing) +const errorResult = myObj({}) +``` + +
diff --git a/ark/docs/src/content/docs/reference/scopes.md b/ark/docs/src/content/docs/scopes.mdx similarity index 89% rename from ark/docs/src/content/docs/reference/scopes.md rename to ark/docs/src/content/docs/scopes.mdx index 611bb4df0e..2b977bf315 100644 --- a/ark/docs/src/content/docs/reference/scopes.md +++ b/ark/docs/src/content/docs/scopes.mdx @@ -12,8 +12,6 @@ A scope is just like a scope in code- it's a resolution space where you can defi A lot of the time, if you don't need to create additional types in your scope, you can just `.export()` it right away. -Here's a quick preview of what that looks like: - ```ts import { scope } from "arktype" @@ -44,3 +42,19 @@ packageData.dependencies![0].dependencies = [packageData] export const out = types.package(packageData) ``` + +### modules + +🚧 Coming soon ™️🚧 + +### visibility + +🚧 Coming soon ™️🚧 + +### submodules + +🚧 Coming soon ™️🚧 + +### thunks + +🚧 Coming soon ™️🚧 diff --git a/ark/docs/src/content/docs/tuples.mdx b/ark/docs/src/content/docs/tuples.mdx deleted file mode 100644 index ef2dfda88d..0000000000 --- a/ark/docs/src/content/docs/tuples.mdx +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: Tuples ---- - -import { Tabs } from "@astrojs/starlight/components" -import SyntaxTab from "../../components/SyntaxTab.astro" - -Like objects, tuples are structures whose values are nested definitions. Like TypeScript, ArkType supports prefix, optional, variadic, and postfix elements, with the same restrictions about combining them. - -### Prefix Elements - - - - -```ts -const myTuple = type([ - "string", - // Object definitions can be nested in tuples- and vice versa! - { - coordinates: ["number", "number"] - } -]) -``` - - - - - -```ts -const myTuple = type([ - type.string, - // Object definitions can be nested in tuples- and vice versa! - { - coordinates: [type.number, type.number] - } -]) -``` - - - - - -### Optional Elements - -Tuples can include any number of optional elements following its prefix elements. - -Like in TypeScript, optional elements are mutually exclusive with postfix elements. - - - - -```ts -const myTuple = type(["string", "boolean?", "number?"]) -``` - - - - - -```ts -const myTuple = type([ - type.string, - type.boolean.optional(), - type.number.optional() -]) -``` - - - - - -```ts -const myTuple = type([ - "string", - [ - { - name: "string" - }, - "?" - ] -]) -``` - - - - - -```ts -const myTuple = type([ - "string", - type( - { - name: "string" - }, - "?" - ) -]) -``` - - - - - -### Variadic Elements - -Like in TypeScript, variadic elements allow zero or more consecutive values of a given type and may occur at most once in a tuple. - -They are specified with a `"..."` operator preceding an array element. - - - - -```ts -// allows a string followed by zero or more numbers -const myTuple = type(["string", "...", "number[]"]) -``` - - - - - -```ts -// allows a string followed by zero or more numbers -const myTuple = type([type.string, "...", type.number.array()]) -``` - - - - - -### Postfix Elements - -Postfix elements are required elements following a variadic element. - -They are mutually exclusive with optional elements. - - - - -```ts -// allows zero or more numbers followed by a boolean, then a string -const myTuple = type(["...", "number[]", "boolean", "string"]) -``` - - - - - -```ts -// allows zero or more numbers followed by a boolean, then a string -const myTuple = type(["...", type.number.array(), type.boolean, type.string]) -``` - - - - diff --git a/ark/docs/src/content/docs/types.mdx b/ark/docs/src/content/docs/types.mdx new file mode 100644 index 0000000000..897561404e --- /dev/null +++ b/ark/docs/src/content/docs/types.mdx @@ -0,0 +1,13 @@ +--- +title: Types +--- + +🚧 Coming soon ™️🚧 + +### Properties + +🚧 Coming soon ™️🚧 + +### Utilities + +🚧 Coming soon ™️🚧 diff --git a/ark/docs/src/styles.css b/ark/docs/src/styles.css index 2c3e6fb549..d22b88bd06 100644 --- a/ark/docs/src/styles.css +++ b/ark/docs/src/styles.css @@ -168,6 +168,10 @@ h1 { font-weight: 400; } +h2 { + color: var(--sl-color-accent-high) !important; +} + header * { /* Ensure navbar items are in front of boat animation but slightly transparent */ z-index: 12; diff --git a/ark/fs/package.json b/ark/fs/package.json index 70b0911179..40e7087aff 100644 --- a/ark/fs/package.json +++ b/ark/fs/package.json @@ -1,6 +1,6 @@ { "name": "@ark/fs", - "version": "0.23.0", + "version": "0.24.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/repo/package.json b/ark/repo/package.json index 681ba63824..b5cfd685a7 100644 --- a/ark/repo/package.json +++ b/ark/repo/package.json @@ -9,7 +9,8 @@ "arktype": "workspace:*", "type-fest": "4.26.1", "typescript": "catalog:", - "zod": "3.23.8" + "zod": "3.23.8", + "@trpc/server": "10.45.2" }, "scripts": { "build": "echo No build required!" diff --git a/ark/repo/scratch.ts b/ark/repo/scratch.ts index 83e5165d20..e00bc67dc7 100644 --- a/ark/repo/scratch.ts +++ b/ark/repo/scratch.ts @@ -1 +1,11 @@ import { type } from "arktype" + +const _user = type({ + name: "string | undefined" +}) + +interface User extends type.infer {} + +const user: type = _user + +user({}).toString() diff --git a/ark/schema/package.json b/ark/schema/package.json index b2be760bfb..83ff354343 100644 --- a/ark/schema/package.json +++ b/ark/schema/package.json @@ -1,6 +1,6 @@ { "name": "@ark/schema", - "version": "0.23.0", + "version": "0.24.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/schema/roots/root.ts b/ark/schema/roots/root.ts index 5511ac9039..c702e534f8 100644 --- a/ark/schema/roots/root.ts +++ b/ark/schema/roots/root.ts @@ -51,10 +51,7 @@ import { import { intersectNodesRoot, pipeNodesRoot } from "../shared/intersections.ts" import type { JsonSchema } from "../shared/jsonSchema.ts" import { $ark } from "../shared/registry.ts" -import type { - ArkTypeStandardSchemaProps, - StandardSchema -} from "../shared/standardSchema.ts" +import type { StandardSchemaV1 } from "../shared/standardSchema.ts" import { arkKind, hasArkKind } from "../shared/utils.ts" import { assertDefaultValueAssignability } from "../structure/optional.ts" import type { Prop } from "../structure/prop.ts" @@ -75,7 +72,7 @@ export abstract class BaseRoot< out d extends InternalRootDeclaration = InternalRootDeclaration > extends BaseNode - implements StandardSchema + implements StandardSchemaV1 { declare readonly [arkKind]: "root" declare readonly [inferred]: unknown @@ -95,7 +92,7 @@ export abstract class BaseRoot< return this } - get "~standard"(): ArkTypeStandardSchemaProps { + get "~standard"(): StandardSchemaV1.ArkTypeProps { return { vendor: "arktype", version: 1, diff --git a/ark/schema/roots/union.ts b/ark/schema/roots/union.ts index 6752adbaec..8353cc47ba 100644 --- a/ark/schema/roots/union.ts +++ b/ark/schema/roots/union.ts @@ -385,7 +385,7 @@ export class UnionNode extends BaseRoot { get nestableExpression(): string { // avoid adding unnecessary parentheses around boolean since it's // already collapsed to a single keyword - return this.isBoolean ? "boolean" : super.nestableExpression + return this.isBoolean ? "boolean" : `(${this.expression})` } discriminate(): Discriminant | null { diff --git a/ark/schema/shared/errors.ts b/ark/schema/shared/errors.ts index a8a0864d7c..00b210b402 100644 --- a/ark/schema/shared/errors.ts +++ b/ark/schema/shared/errors.ts @@ -12,7 +12,7 @@ import { import type { ResolvedArkConfig } from "../config.ts" import type { Prerequisite, errorContext } from "../kinds.ts" import type { NodeKind } from "./implement.ts" -import type { StandardFailureResult } from "./standardSchema.ts" +import type { StandardSchemaV1 } from "./standardSchema.ts" import type { TraversalContext } from "./traversal.ts" import { arkKind } from "./utils.ts" @@ -40,10 +40,9 @@ export class ArkError< } this.nodeConfig = ctx.config[this.code] as never this.path = - input.relativePath ? - new ReadonlyPath([...ctx.path, ...input.relativePath]) - : input.path ? new ReadonlyPath(input.path) - : new ReadonlyPath(ctx.path) + input.relativePath ? new ReadonlyPath(...ctx.path, ...input.relativePath) + : input.path ? new ReadonlyPath(...input.path) + : new ReadonlyPath(...ctx.path) this.data = "data" in input ? input.data : data } @@ -84,7 +83,7 @@ export class ArkError< export class ArkErrors extends ReadonlyArray - implements StandardFailureResult + implements StandardSchemaV1.FailureResult { protected ctx: TraversalContext diff --git a/ark/schema/shared/standardSchema.ts b/ark/schema/shared/standardSchema.ts index 706fb6f86c..069aaf1bb0 100644 --- a/ark/schema/shared/standardSchema.ts +++ b/ark/schema/shared/standardSchema.ts @@ -3,107 +3,107 @@ /** * The Standard Schema interface. */ -export interface StandardSchema { +export interface StandardSchemaV1 { /** * The Standard Schema properties. */ - readonly "~standard": StandardSchemaProps + readonly "~standard": StandardSchemaV1.Props } -/** - * The Standard Schema properties interface. - */ -export interface StandardSchemaProps { - /** - * The version number of the standard. - */ - readonly version: 1 - /** - * The vendor name of the schema library. - */ - readonly vendor: string - /** - * Validates unknown input values. - */ - readonly validate: ( - value: unknown - ) => StandardResult | Promise> +export declare namespace StandardSchemaV1 { /** - * Inferred types associated with the schema. + * The Standard Schema properties interface. */ - readonly types?: StandardTypes | undefined -} - -export interface ArkTypeStandardSchemaProps - extends StandardSchemaProps { - readonly vendor: "arktype" -} + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1 + /** + * The vendor name of the schema library. + */ + readonly vendor: string + /** + * Validates unknown input values. + */ + readonly validate: ( + value: unknown + ) => Result | Promise> + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined + } -/** - * The result interface of the validate function. - */ -type StandardResult = - | StandardSuccessResult - | StandardFailureResult + export interface ArkTypeProps + extends Props { + readonly vendor: "arktype" + } -/** - * The result interface if validation succeeds. - */ -interface StandardSuccessResult { - /** - * The typed output value. - */ - readonly value: Output /** - * The non-existent issues. + * The result interface of the validate function. */ - readonly issues?: undefined -} + type Result = SuccessResult | FailureResult -/** - * The result interface if validation fails. - */ -export interface StandardFailureResult { /** - * The issues of failed validation. + * The result interface if validation succeeds. */ - readonly issues: ReadonlyArray -} + interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output + /** + * The non-existent issues. + */ + readonly issues?: undefined + } -/** - * The issue interface of the failure output. - */ -export interface StandardIssue { - /** - * The error message of the issue. - */ - readonly message: string /** - * The path of the issue, if any. + * The result interface if validation fails. */ - readonly path?: ReadonlyArray | undefined -} + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray + } -/** - * The path segment interface of the issue. - */ -interface StandardPathSegment { /** - * The key representing a path segment. + * The issue interface of the failure output. */ - readonly key: PropertyKey -} + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined + } -/** - * The base types interface of Standard Schema. - */ -interface StandardTypes { /** - * The input type of the schema. + * The path segment interface of the issue. */ - readonly input: Input + interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey + } + /** - * The output type of the schema. + * The base types interface of Standard Schema. */ - readonly output: Output + interface Types { + /** + * The input type of the schema. + */ + readonly input: Input + /** + * The output type of the schema. + */ + readonly output: Output + } } diff --git a/ark/schema/shared/traversal.ts b/ark/schema/shared/traversal.ts index cb4a5d3ad9..672575e964 100644 --- a/ark/schema/shared/traversal.ts +++ b/ark/schema/shared/traversal.ts @@ -42,7 +42,7 @@ export class TraversalContext { queueMorphs(morphs: array): void { const input: MorphsAtPath = { - path: new ReadonlyPath(this.path), + path: new ReadonlyPath(...this.path), morphs } if (this.currentBranch) this.currentBranch.queuedMorphs.push(input) diff --git a/ark/type/__tests__/objects/spread.test.ts b/ark/type/__tests__/objects/merge.test.ts similarity index 100% rename from ark/type/__tests__/objects/spread.test.ts rename to ark/type/__tests__/objects/merge.test.ts diff --git a/ark/type/__tests__/bounds.test.ts b/ark/type/__tests__/range.test.ts similarity index 100% rename from ark/type/__tests__/bounds.test.ts rename to ark/type/__tests__/range.test.ts diff --git a/ark/type/__tests__/realWorld.test.ts b/ark/type/__tests__/realWorld.test.ts index 1124524e8e..937bf23cc4 100644 --- a/ark/type/__tests__/realWorld.test.ts +++ b/ark/type/__tests__/realWorld.test.ts @@ -1042,4 +1042,9 @@ nospace must be matched by ^\\S*$ (was "One space")`) '{ storeA: { [string]: string }, ext?: string = ".txt" } | { storeB: { foo: { [string]: string } }, ext?: string = ".txt" }' ) }) + + it("correct toString for array of union", () => { + const t = type("(string | number)[]") + attest(t.expression).snap("(number | string)[]") + }) }) diff --git a/ark/type/generic.ts b/ark/type/generic.ts index b16cf7865a..62e39c96f9 100644 --- a/ark/type/generic.ts +++ b/ark/type/generic.ts @@ -407,7 +407,8 @@ export type GenericParser<$ = {}> = < readonly [infer name, infer def] ) ? readonly [name, type.validate] - : paramsDef[i] + : // if the param is only a name, no validation is required + paramsDef[i] } ) => GenericBodyParser, $> diff --git a/ark/type/index.ts b/ark/type/index.ts index 0546ec567d..bcb2576de3 100644 --- a/ark/type/index.ts +++ b/ark/type/index.ts @@ -6,6 +6,7 @@ export { type JsonSchema } from "@ark/schema" export { Hkt, inferred } from "@ark/util" +export type { distill } from "./attributes.ts" export { Generic } from "./generic.ts" export { ark, diff --git a/ark/type/keywords/string/string.ts b/ark/type/keywords/string/string.ts index 28db92773e..d590dfb973 100644 --- a/ark/type/keywords/string/string.ts +++ b/ark/type/keywords/string/string.ts @@ -41,25 +41,25 @@ import { uuid } from "./uuid.ts" export const string = arkModule({ root: intrinsic.string, - numeric, - integer, alpha, alphanumeric, base64, - digits, - semver, - ip, + capitalize, creditCard, + date: stringDate, + digits, email, - uuid, - url, + integer, + ip, json, - trim, - upper, lower, normalize, - capitalize, - date: stringDate + numeric, + semver, + trim, + upper, + url, + uuid }) export type Matching = { @@ -121,22 +121,22 @@ export declare namespace string { alpha: alpha alphanumeric: alphanumeric base64: base64.submodule - digits: digits - numeric: stringNumeric.submodule - integer: stringInteger.submodule + capitalize: capitalize.submodule creditCard: creditCard + date: stringDate.submodule + digits: digits email: email - uuid: uuid.submodule - semver: semver + integer: stringInteger.submodule ip: ip.submodule json: stringJson.submodule - date: stringDate.submodule - url: url.submodule - trim: trim.submodule - normalize: normalize.submodule - capitalize: capitalize.submodule lower: lower.submodule + normalize: normalize.submodule + numeric: stringNumeric.submodule + semver: semver + trim: trim.submodule upper: upper.submodule + url: url.submodule + uuid: uuid.submodule } export type branded = brand> diff --git a/ark/type/keywords/ts.ts b/ark/type/keywords/ts.ts index 4a661aab81..5b5ac52e23 100644 --- a/ark/type/keywords/ts.ts +++ b/ark/type/keywords/ts.ts @@ -163,12 +163,12 @@ const Extract = genericNode("T", "U")( ) export const arkTsGenerics: arkTsGenerics.module = arkModule({ - Record, - Pick, - Omit, Exclude, Extract, + Omit, Partial, + Pick, + Record, Required }) @@ -178,12 +178,12 @@ export declare namespace arkTsGenerics { export type submodule = Submodule<$> export type $ = { - Record: typeof Record.t - Pick: typeof Pick.t - Omit: typeof Omit.t Exclude: typeof Exclude.t Extract: typeof Extract.t + Omit: typeof Omit.t Partial: typeof Partial.t + Pick: typeof Pick.t + Record: typeof Record.t Required: typeof Required.t } } diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index ee641060ba..ad4afab821 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -1,6 +1,5 @@ import type { ArkErrors, - ArkTypeStandardSchemaProps, BaseRoot, Disjoint, JsonSchema, @@ -8,6 +7,7 @@ import type { Morph, Predicate, PredicateCast, + StandardSchemaV1, UndeclaredKeyBehavior } from "@ark/schema" import type { @@ -43,10 +43,7 @@ import type { instantiateType } from "./instantiate.ts" /** @ts-ignore cast variance */ interface Type - extends Callable< - (data: unknown) => distill.Out | ArkErrors, - ArkTypeStandardSchemaProps - > { + extends Callable<(data: unknown) => distill.Out | ArkErrors> { [inferred]: t // The top-level generic parameter accepted by the `Type`. Potentially @@ -285,7 +282,7 @@ interface Type ): instantiateType // Standard Schema Compatibility (https://github.com/standard-schema/standard-schema) - "~standard": ArkTypeStandardSchemaProps + "~standard": StandardSchemaV1.ArkTypeProps // deprecate Function methods so they are deprioritized as suggestions diff --git a/ark/type/package.json b/ark/type/package.json index f7b3f1e796..7505940271 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.23", + "version": "2.0.0-rc.24", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/util/__tests__/path.test.ts b/ark/util/__tests__/path.test.ts new file mode 100644 index 0000000000..4d4efeb05f --- /dev/null +++ b/ark/util/__tests__/path.test.ts @@ -0,0 +1,16 @@ +import { attest, contextualize } from "@ark/attest" +import { ReadonlyPath } from "@ark/util" + +contextualize(() => { + it("creates an array of a single number", () => { + const path = new ReadonlyPath(5) + + attest([...path]).snap([5]) + }) + + it("arary methods preserve subclass", () => { + const path = new ReadonlyPath() + + attest(path.slice()).instanceOf(ReadonlyPath) + }) +}) diff --git a/ark/util/package.json b/ark/util/package.json index ace5135acc..f9c92e827a 100644 --- a/ark/util/package.json +++ b/ark/util/package.json @@ -1,6 +1,6 @@ { "name": "@ark/util", - "version": "0.23.0", + "version": "0.24.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/util/path.ts b/ark/util/path.ts index f3fbb424ed..55950ca876 100644 --- a/ark/util/path.ts +++ b/ark/util/path.ts @@ -71,11 +71,10 @@ export class ReadonlyPath extends ReadonlyArray { stringifyAncestors?: readonly string[] } = {} - constructor(items: array) { + constructor(...items: array) { super() // avoid case where a single number will create empty slots ;(this as any).push(...items) - Object.freeze(this) } stringify(): string { diff --git a/ark/util/registry.ts b/ark/util/registry.ts index 4cc94cbecd..f504e9ee5d 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.23.0" +export const arkUtilVersion = "0.24.0" export const initialRegistryContents = { version: arkUtilVersion, diff --git a/package.json b/package.json index 7b0f87fa61..06a8293f60 100644 --- a/package.json +++ b/package.json @@ -51,23 +51,23 @@ "@ark/fs": "workspace:*", "@ark/repo": "workspace:*", "@ark/util": "workspace:*", - "@eslint/js": "9.12.0", + "@eslint/js": "9.14.0", "@standard-schema/spec": "1.0.0-beta.3", "@types/mocha": "10.0.9", - "@types/node": "22.7.5", + "@types/node": "22.9.0", "arktype": "workspace:*", "c8": "10.1.2", - "eslint": "9.12.0", + "eslint": "9.14.0", "eslint-plugin-import": "2.31.0", "eslint-plugin-only-warn": "1.1.0", "eslint-plugin-prefer-arrow-functions": "3.4.1", - "knip": "5.33.3", - "mocha": "10.7.3", + "knip": "5.37.0", + "mocha": "10.8.2", "prettier": "3.3.3", "prettier-plugin-astro": "0.14.1", - "tsx": "4.19.1", + "tsx": "4.19.2", "typescript": "catalog:", - "typescript-eslint": "8.9.0" + "typescript-eslint": "8.14.0" }, "mocha": { "//": "IF YOU UPDATE THE MOCHA CONFIG HERE, PLEASE ALSO UPDATE ark/repo/mocha.jsonc AND .vscode/settings.json", @@ -116,5 +116,5 @@ } } }, - "packageManager": "pnpm@9.9.0" + "packageManager": "pnpm@9.13.2" } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b01f44a78f..2f5f14f147 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,6 @@ packages: - "ark/*" catalog: - typescript: 5.7.0-beta + typescript: 5.7.1-rc "@ark/attest-ts-min": npm:typescript@5.1.6 "@ark/attest-ts-next": npm:typescript@next