diff --git a/ark/dark/injected.tmLanguage.json b/ark/dark/injected.tmLanguage.json index 41443833bc..1ea08f9f22 100644 --- a/ark/dark/injected.tmLanguage.json +++ b/ark/dark/injected.tmLanguage.json @@ -11,7 +11,7 @@ "repository": { "arkDefinition": { "contentName": "meta.embedded.arktype.definition", - "begin": "(([^\\)\\(\\s]+)?(?:type|scope|define|match|fn|\\.morph|\\.and|\\.or|\\.when))(\\()", + "begin": "(([^\\)\\(\\s]+)?(?:type|scope|define|match|fn|\\.morph|\\.and|\\.or|\\.when|\\.extends|\\.intersect))(\\()", "beginCaptures": { "1": { "name": "entity.name.function.ts" diff --git a/ark/dark/package.json b/ark/dark/package.json index cb962960e2..32d8cddce8 100644 --- a/ark/dark/package.json +++ b/ark/dark/package.json @@ -2,7 +2,7 @@ "name": "arkdark", "displayName": "ArkDark", "description": "ArkType syntax highlighting and themeā›µ", - "version": "5.1.6", + "version": "5.1.7", "publisher": "arktypeio", "type": "module", "scripts": { diff --git a/ark/dark/tsWithArkType.tmLanguage.json b/ark/dark/tsWithArkType.tmLanguage.json index d2ee4687cf..99018645a0 100644 --- a/ark/dark/tsWithArkType.tmLanguage.json +++ b/ark/dark/tsWithArkType.tmLanguage.json @@ -22,7 +22,7 @@ "repository": { "arkDefinition": { "contentName": "meta.embedded.arktype.definition", - "begin": "(([^\\)\\(\\s]+)?(?:type|scope|define|match|fn|\\.morph|\\.and|\\.or|\\.when))(\\()", + "begin": "(([^\\)\\(\\s]+)?(?:type|scope|define|match|fn|\\.morph|\\.and|\\.or|\\.when|\\.extends|\\.intersect))(\\()", "beginCaptures": { "1": { "name": "entity.name.function.ts" diff --git a/ark/docs/astro.config.js b/ark/docs/astro.config.js index 3973a852a6..d24d77f9ce 100644 --- a/ark/docs/astro.config.js +++ b/ark/docs/astro.config.js @@ -3,7 +3,7 @@ import react from "@astrojs/react" import starlight from "@astrojs/starlight" import { defineConfig } from "astro/config" -import { shikiConfig } from "./shiki.config.js" +import { shikiConfig } from "./src/components/shiki.config.js" // https://astro.build/config export default defineConfig({ diff --git a/ark/docs/shiki.config.js b/ark/docs/shiki.config.js deleted file mode 100644 index a8d90fb4c9..0000000000 --- a/ark/docs/shiki.config.js +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-check - -import { transformerTwoslash } from "@shikijs/twoslash" -import arkdarkColors from "arkdark/color-theme.json" -import arktypeTextmate from "arkdark/tsWithArkType.tmLanguage.json" -import { defaultCompilerOptions } from "twoslash" - -// Theme adjustments - -arkdarkColors.colors["editor.background"] = "#00000027" -// @ts-expect-error -arkdarkColors.tokenColors.push({ - // this is covered by editorBracketHighlight.foreground1 etc. in VSCode, - // but it's not available in Shiki so add a replacement - scope: ["meta.brace"], - settings: { - foreground: "#f5cf8f" - } -}) - -const twoslashPropertyPrefix = "(property) " - -export const twoslash = transformerTwoslash({ - langs: ["ts", "js"], - twoslashOptions: { - compilerOptions: { - ...defaultCompilerOptions, - exactOptionalPropertyTypes: true - }, - filterNode: node => { - if (node.type !== "hover") return true - if (node.text.startsWith("const")) { - if (node.text.endsWith(", {}>")) - // omit default scope param from type display - node.text = node.text.slice(0, -5) + ">" - // filter out the type of Type's invocation - // as opposed to the Type itself - return !node.text.includes("(data: unknown)") - } - if (node.text.startsWith(twoslashPropertyPrefix)) { - const expression = node.text.slice(twoslashPropertyPrefix.length) - if (expression.startsWith("ArkErrors.summary")) - // this helps demonstrate narrowing on discrimination - return true - if (expression.endsWith("typeof ArkErrors")) - // also helps clarify how discrimination works - return true - return false - } - return false - } - } -}) - -/** @type { import("astro").ShikiConfig } */ -export const shikiConfig = { - theme: arkdarkColors, - // @ts-expect-error - langs: [arktypeTextmate], - transformers: [twoslash] -} diff --git a/ark/docs/src/assets/HowToUpdateArktypeGifs.md b/ark/docs/src/assets/HowToUpdateArktypeGifs.md deleted file mode 100644 index 895dbc276a..0000000000 --- a/ark/docs/src/assets/HowToUpdateArktypeGifs.md +++ /dev/null @@ -1,28 +0,0 @@ -# How to update these GIFs - -1. Used modified VSCode: - - Deleted fileName navbar and added `margin-top: 24px` to the editor from devtools within VsCode - - Enabled Quokka, but disabled "Show Expression Value on Select" (this has to be done every time Quokka is restarted) - - Applied these settings: - -```ts -// DEMO ONLY (revert) -// previously 225 -"glassit.alpha": 255, -// previously 1 -"window.zoomLevel": 2, -// previously smooth -"editor.cursorBlinking": "solid", -"scm.diffDecorations": "none", -"breadcrumbs.enabled": false, -"editor.minimap.enabled": false, -"editor.scrollBeyondLastLine": false, -// "typescript.suggest.enabled": false, -``` - -2. Once finished recording, generate a high-quality MP4 from ClipChamp called `arktype.mp4`. This MP4 should have a #1b1b1b background, and can be used directly on arktype.io. -3. Import the new MP4 into your existing VSDC project -4. Generate `arktype.apng` as a 1750x750 100% quality MP4/MOV/APNG etc. based on the existing project (has transparency filters etc.). The GitHub version should have a background of #0d1117, exactly matching GitHub in dark mode. You should experiment with multiple output formats until you ensure that #0d1117 is exactly the background. E.g. for GIFs, you can check the palette to ensure it is there ahead of time. -5. Check `arktype.apng` to ensure it is exactly #0d1117, then save the VSDC project -6. Run `arktype.apng` through [GifTuna](https://github.com/dudewheresmycode/giftuna) with "Dither" disabled. Ensure the output background is still the same and that the GIF is ~20MB, and name the result `arktype.gif`. -7. Replace `arktype.mp4` and `arktype.gif` in (repo-root)/dev/arktype.io/static/img. diff --git a/ark/docs/src/components/BenchmarksGraph.tsx b/ark/docs/src/components/BenchmarksGraph.tsx new file mode 100644 index 0000000000..b66db13d25 --- /dev/null +++ b/ark/docs/src/components/BenchmarksGraph.tsx @@ -0,0 +1,99 @@ +import React from "react" + +const barStyles: React.CSSProperties = { + height: "30px", + borderRadius: "5px", + display: "flex", + alignItems: "baseline", + marginRight: "1rem", + color: "black" +} + +const arkBarStyles = { + ...barStyles, + background: + "repeating-linear-gradient(135deg, #40decc, #40decc 10px, #34c8b9 10px, #34c8b9 20px)" +} + +const zodBarStyles = { + ...barStyles, + background: + "repeating-linear-gradient(135deg, #b084f6, #b084f6 10px, #9a6fe3 10px, #9a6fe3 20px)" +} + +export const RuntimeBenchmarksGraph = () => ( +
+
+
+ Node v22.2.0 ( + + source + + ) +
+
+
+   ArkType (68,581,169 ops) +
+
+
+ Zod (727,703 ops) +
+
+) + +export const TypeBenchmarksGraph = () => ( +
+
+
+ Union Type Instantiations, TypeScript 5.4.5 ( + + source + + ) +
+
+
+
+ ArkType Auto-Discriminated (7,801) +
+
+
+ Zod Raw (24,944) +
+
+ Zod Discriminated (71,312)   +
+
+) diff --git a/ark/docs/src/components/highlight.ts b/ark/docs/src/components/highlight.ts index c9b5a1551f..9b01c79cea 100644 --- a/ark/docs/src/components/highlight.ts +++ b/ark/docs/src/components/highlight.ts @@ -1,7 +1,7 @@ import arkdarkColors from "arkdark/color-theme.json" import arktypeTextmate from "arkdark/tsWithArkType.tmLanguage.json" import { getHighlighter } from "shiki" -import { twoslash } from "../../shiki.config.js" +import { twoslash } from "./shiki.config.js" let highlighter: Awaited> | undefined diff --git a/ark/docs/src/components/shiki.config.js b/ark/docs/src/components/shiki.config.js new file mode 100644 index 0000000000..8cb3314b91 --- /dev/null +++ b/ark/docs/src/components/shiki.config.js @@ -0,0 +1,109 @@ +// @ts-check + +import { transformerTwoslash } from "@shikijs/twoslash" +import arkdarkColors from "arkdark/color-theme.json" +import arkdarkPackageJson from "arkdark/package.json" +import arktypeTextmate from "arkdark/tsWithArkType.tmLanguage.json" +import { defaultCompilerOptions } from "twoslash" + +// Theme adjustments + +arkdarkColors.colors["editor.background"] = "#00000027" +// @ts-expect-error +arkdarkColors.tokenColors.push({ + // this is covered by editorBracketHighlight.foreground1 etc. in VSCode, + // but it's not available in Shiki so add a replacement + scope: ["meta.brace"], + settings: { + foreground: "#f5cf8f" + } +}) + +const twoslashPropertyPrefix = "(property) " + +export const twoslash = transformerTwoslash({ + langs: ["ts", "js"], + twoslashOptions: { + compilerOptions: { + ...defaultCompilerOptions, + exactOptionalPropertyTypes: true + }, + filterNode: node => { + switch (node.type) { + case "hover": + if (node.text.startsWith("const")) { + if (node.text.endsWith(", {}>")) + // omit default scope param from type display + node.text = node.text.slice(0, -5) + ">" + // show type with completions populated for known examples + node.text = node.text.replace( + "isAdmin?: never", + "isAdmin?: boolean | null" + ) + node.text = node.text.replace( + "luckyNumbers: never", + "luckyNumbers: (number | bigint)[]" + ) + // filter out the type of Type's invocation + // as opposed to the Type itself + return !node.text.includes("(data: unknown)") + } + if (node.text.startsWith(twoslashPropertyPrefix)) { + const expression = node.text.slice(twoslashPropertyPrefix.length) + if (expression.startsWith("ArkErrors.summary")) { + // cleanup runtime errors for display + const runtimeErrorSummary = /^ArkErrors\.summary: "(.*)"/.exec( + expression + ) + if (runtimeErrorSummary) { + node.text = runtimeErrorSummary[1].split("\\n").join("\n") + } + // this helps demonstrate narrowing on discrimination + return true + } + if (expression === "luckyNumbers: (number | bigint)[]") + // this helps demonstrate narrowing on discrimination + return true + if (expression.endsWith("typeof ArkErrors")) + // also helps clarify how discrimination works + return true + return false + } + return false + case "error": + // adapted from my ErrorLens implementation at + // https://github.com/usernamehw/vscode-error-lens/blob/d1786ddeedee23d70f5f75b16415a6579b554b59/src/utils/extUtils.ts#L127 + for (const transformation of arkdarkPackageJson.contributes + .configurationDefaults["errorLens.replace"]) { + const regex = new RegExp(transformation.matcher) + const matchResult = regex.exec(node.text) + if (matchResult) { + node.text = transformation.message + // Replace groups like $0 and $1 with groups from the match + for ( + let groupIndex = 0; + groupIndex < matchResult.length; + groupIndex++ + ) { + node.text = node.text.replace( + new RegExp(`\\$${groupIndex}`, "gu"), + matchResult[Number(groupIndex)] + ) + } + node.text = `TypeScript: ${node.text}` + } + } + default: + return true + } + } + } +}) + +/** @type { import("astro").ShikiConfig } */ +export const shikiConfig = { + theme: arkdarkColors, + // @ts-expect-error + langs: [arktypeTextmate], + transformers: [twoslash] +} diff --git a/ark/docs/src/content/docs/betterErrors.twoslash.ts b/ark/docs/src/content/docs/betterErrors.twoslash.ts new file mode 100644 index 0000000000..719e279d8b --- /dev/null +++ b/ark/docs/src/content/docs/betterErrors.twoslash.ts @@ -0,0 +1,29 @@ +import { type } from "arktype" + +const user = type({ + name: "string", + luckyNumbers: "(number | bigint)[]", + "isAdmin?": "boolean | null" +}) + +// ---cut--- +const out = user({ + luckyNumbers: [31, "255", 1337n], + isAdmin: 1 +}) + +if (out instanceof type.errors) { + // ---cut-start--- + // just a trick to display the runtime error + if ( + out.summary !== + `luckyNumbers[1] must be a bigint or a number (was string) +name must be a string (was missing) +isAdmin must be false, null or true (was 1)` + ) + throw new Error() + + // ---cut-end--- + // hover out.summary to see validation errors + console.error(out.summary) +} else console.log(out.luckyNumbers) diff --git a/ark/docs/src/content/docs/clearAndConcise.twoslash.ts b/ark/docs/src/content/docs/clearAndConcise.twoslash.ts new file mode 100644 index 0000000000..cbda5e6277 --- /dev/null +++ b/ark/docs/src/content/docs/clearAndConcise.twoslash.ts @@ -0,0 +1,9 @@ +// @errors: 2322 +import { type } from "arktype" +// ---cut--- +// hover me +const user = type({ + name: "string", + luckyNumbers: "number | bigint)[]", + "isAdmin?": "boolean | null" +}) diff --git a/ark/docs/src/content/docs/deepIntrospectability.twoslash.ts b/ark/docs/src/content/docs/deepIntrospectability.twoslash.ts new file mode 100644 index 0000000000..844f8cae8b --- /dev/null +++ b/ark/docs/src/content/docs/deepIntrospectability.twoslash.ts @@ -0,0 +1,26 @@ +import { type } from "arktype" + +const user = type({ + name: "string", + luckyNumbers: "(number | bigint)[]", + "isAdmin?": "boolean | null" +}) + +// ---cut--- +user.extends("object") // true +user.extends("string") // false +// ---cut-start--- +// prettier-ignore +// ---cut-end--- +user.extends({ // true + luckyNumbers: "unknown[]", +}) +// ---cut-start--- +// prettier-ignore +// ---cut-end--- +user.extends({ // false + luckyNumbers: "number[]" +}) + +// get transitively referenced TypeNodes +console.log(user.raw.references) diff --git a/ark/docs/src/content/docs/index.mdx b/ark/docs/src/content/docs/index.mdx index 2ea6642a10..96377abecb 100644 --- a/ark/docs/src/content/docs/index.mdx +++ b/ark/docs/src/content/docs/index.mdx @@ -18,8 +18,18 @@ hero: icon: external --- -import { Card, CardGrid } from "@astrojs/starlight/components" +import { Card, CardGrid, TabItem, Tabs } from "@astrojs/starlight/components" +import { + RuntimeBenchmarksGraph, + TypeBenchmarksGraph +} from "../../components/BenchmarksGraph.tsx" import { HeroBackdrop } from "../../components/HeroBackdrop.tsx" +import TsBlock from "../../components/TsBlock.astro" +import betterErrorsSrc from "./betterErrors.twoslash.ts?raw" +import clearAndConciseSrc from "./clearAndConcise.twoslash.ts?raw" +import deepIntrospectability from "./deepIntrospectability.twoslash.ts?raw" +import intrinsicOptimization from "./intrinsicOptimization.twoslash.ts?raw" +import unparalleledDxSrc from "./unparalleledDx.twoslash.ts?raw"
@@ -29,27 +39,40 @@ import { HeroBackdrop } from "../../components/HeroBackdrop.tsx" - Type syntax you already know with safety unlike anything you've ever seen + Type syntax you already know with safety and completions unlike anything + you've ever seen + - - [100x faster than - Zod](https://moltar.github.io/typescript-runtime-type-benchmarks/) with - editor performance that will remind you how autocomplete is supposed to feel + + 100x faster than Zod at runtime with editor performance that will remind you + how autocomplete is supposed to feel + + + + + + + + - - Schemas are half as long and twice as readable with hovers that tell you - just what really matters + + Definitions are half as long, type errors are twice as readable, and hovers + tell you just what really matters + - - Deeply customizable and composable messages with great defaults + + Deeply customizable messages with great defaults + - + ArkType uses set theory to understand and expose the relationships between your types at runtime the way TypeScript does at compile time + - - Every schema you define is internally optimized to provide the clearest, - fastest validation possible + + Every schema is internally normalized and reduced to its purest and fastest + representation + {/* Most definitions are just objects and strings- take them across the stack or diff --git a/ark/docs/src/content/docs/intrinsicOptimization.twoslash.ts b/ark/docs/src/content/docs/intrinsicOptimization.twoslash.ts new file mode 100644 index 0000000000..c8fbcfb6be --- /dev/null +++ b/ark/docs/src/content/docs/intrinsicOptimization.twoslash.ts @@ -0,0 +1,15 @@ +import { type } from "arktype" +// ---cut--- +// all unions are optimally discriminated- +// even at nested paths or in multiple passes! +const account = type({ + kind: "'admin'", + "powers?": "string[]" +}) + .or({ + kind: "'superadmin'", + "superpowers?": "string[]" + }) + .or({ + kind: "'pleb'" + }) diff --git a/ark/docs/src/content/docs/unparalleledDx.twoslash.ts b/ark/docs/src/content/docs/unparalleledDx.twoslash.ts new file mode 100644 index 0000000000..4536595573 --- /dev/null +++ b/ark/docs/src/content/docs/unparalleledDx.twoslash.ts @@ -0,0 +1,12 @@ +import { type } from "arktype" +// ---cut--- +// hover me +const user = type({ + name: "string", + luckyNumbers: "number[]", + // ---cut-start--- + // @ts-expect-error + // ---cut-end--- + "isAdmin?": "boolean | n" + // ^| +}) diff --git a/ark/docs/src/styles.css b/ark/docs/src/styles.css index af3bec6d9f..abd2c04277 100644 --- a/ark/docs/src/styles.css +++ b/ark/docs/src/styles.css @@ -26,6 +26,9 @@ --sl-color-gray-5: #003b62; --sl-color-gray-6: #002946; --sl-color-black: #001a2f; + /* Based on ArkDark ErrorLens */ + --ark-error: #9558f8a0; + --ark-success: #40decca0; /** @shikijs/twoslash/style-rich.css overrides */ --twoslash-border-color: #ba7e4127; --twoslash-underline-color: currentColor; @@ -42,9 +45,9 @@ --twoslash-code-font-size: 1em; --twoslash-matched-color: inherit; --twoslash-unmatched-color: #888; - --twoslash-cursor-color: #8888; - --twoslash-error-color: #d45656; - --twoslash-error-bg: #d4565620; + --twoslash-cursor-color: var(--sl-color-gray-2); + --twoslash-error-color: var(--ark-error); + --twoslash-error-bg: #9558f818; --twoslash-warn-color: #c37d0d; --twoslash-warn-bg: #c37d0d20; --twoslash-tag-color: #3772cf; @@ -71,7 +74,9 @@ --sl-color-black: #fffff0; } -pre.astro-code { +pre.astro-code, +pre.shiki, +.twoslash-popup-container { border-radius: 1rem; border-color: #ba7e4127; border-width: 1px; @@ -92,17 +97,18 @@ starlight-theme-select, display: none; } -.twoslash .twoslash-hover:hover .twoslash-popup-container { +.twoslash .twoslash-hover:hover .twoslash-popup-container, +.twoslash .twoslash-completion-cursor .twoslash-completion-list { border-radius: 1rem; opacity: 1; - backdrop-filter: blur(10px); - border: 1px solid #ba7e4127; + background: #001323aa; + backdrop-filter: blur(8px); box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); } -/* FireFox specific rules */ +/* Firefox specific rules */ @-moz-document url-prefix() { - /* The backdrop-filter above doesn't work by default yet on FireFox so we do this instead */ + /* The backdrop-filter above doesn't work by default yet on Firefox so we do this instead */ .twoslash .twoslash-hover:hover .twoslash-popup-container { background: #001323ee; } diff --git a/ark/docs/tsconfig.json b/ark/docs/tsconfig.json index 56a73e3ff4..67410a862b 100644 --- a/ark/docs/tsconfig.json +++ b/ark/docs/tsconfig.json @@ -1,6 +1,11 @@ { - "extends": ["astro/tsconfigs/strict"], + "extends": "../../tsconfig.json", "compilerOptions": { - "jsx": "react" - } + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "allowJs": true, + "jsx": "preserve" + }, + "exclude": ["**/out", "**/node_modules"] } diff --git a/ark/repo/.eslintrc.cjs b/ark/repo/.eslintrc.cjs index 9f1b9711ab..57a30496cf 100644 --- a/ark/repo/.eslintrc.cjs +++ b/ark/repo/.eslintrc.cjs @@ -147,7 +147,12 @@ module.exports = defineConfig({ } }, { - files: ["**/__tests__/**", "**/*.bench.ts", "**/*.test.ts"], + files: [ + "**/__tests__/**", + "**/*.bench.ts", + "**/*.test.ts", + "**/*.twoslash.ts" + ], rules: { // Assignment to a variable is required to ensure types are parsed "@typescript-eslint/no-unused-vars": "off", diff --git a/ark/repo/package.json b/ark/repo/package.json index fdee6358bb..c5482311e5 100644 --- a/ark/repo/package.json +++ b/ark/repo/package.json @@ -6,7 +6,8 @@ "arktype": "workspace:*", "@arktype/attest": "workspace:*", "@arktype/fs": "workspace:*", - "ts-morph": "20.0.0" + "ts-morph": "20.0.0", + "zod": "3.23.8" }, "scripts": { "build": "echo No build required!" diff --git a/ark/repo/scratch/discriminatedComparison.ts b/ark/repo/scratch/discriminatedComparison.ts index 30fac84d8d..aedab5885f 100644 --- a/ark/repo/scratch/discriminatedComparison.ts +++ b/ark/repo/scratch/discriminatedComparison.ts @@ -1,6 +1,8 @@ -// import z from "zod" import { bench } from "@arktype/attest" import { type } from "arktype" +import z from "zod" + +/** Measured with typescript@5.4.5, zod@3.23.8, arktype@2.0.0-dev.16 */ bench("arktype", () => { // Union is automatically discriminated using shallow or deep keys @@ -15,21 +17,37 @@ bench("arktype", () => { .or({ kind: "'pleb'" }) -}).types([4182, "instantiations"]) +}).types([7801, "instantiations"]) + +bench("zod", () => { + const user = z.union([ + z.object({ + kind: z.literal("admin"), + powers: z.string().array().optional() + }), + z.object({ + kind: z.literal("superadmin"), + superpowers: z.string().array().optional() + }), + z.object({ + kind: z.literal("pleb") + }) + ]) +}).types([24944, "instantiations"]) -// bench("zod", () => { -// // Union must be manually discriminated using only shallow keys -// const user = z.discriminatedUnion("kind", [ -// z.object({ -// kind: z.literal("admin"), -// powers: z.string().array().optional() -// }), -// z.object({ -// kind: z.literal("superadmin"), -// superpowers: z.string().array().optional() -// }), -// z.object({ -// kind: z.literal("pleb") -// }) -// ]) -// }).types([86522, "instantiations"]) +bench("zod discriminated", () => { + // Union must be manually discriminated using only shallow keys + const user = z.discriminatedUnion("kind", [ + z.object({ + kind: z.literal("admin"), + powers: z.string().array().optional() + }), + z.object({ + kind: z.literal("superadmin"), + superpowers: z.string().array().optional() + }), + z.object({ + kind: z.literal("pleb") + }) + ]) +}).types([71312, "instantiations"]) diff --git a/ark/repo/scratch/realWorldComparison.ts b/ark/repo/scratch/realWorldComparison.ts index eb601d102d..885228ba96 100644 --- a/ark/repo/scratch/realWorldComparison.ts +++ b/ark/repo/scratch/realWorldComparison.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { bench } from "@arktype/attest" import { scope, type } from "arktype" import { z } from "zod" diff --git a/ark/schema/shared/errors.ts b/ark/schema/shared/errors.ts index 4839a03ef4..0dffe32bb7 100644 --- a/ark/schema/shared/errors.ts +++ b/ark/schema/shared/errors.ts @@ -128,7 +128,7 @@ export class ArkErrors extends ReadonlyArray { } throw(): never { - throw this + throw new AggregateError(this, this.message) } } diff --git a/ark/type/CHANGELOG.md b/ark/type/CHANGELOG.md index 9f4fac5d68..1bb9241e91 100644 --- a/ark/type/CHANGELOG.md +++ b/ark/type/CHANGELOG.md @@ -1,5 +1,9 @@ # arktype +## 2.0.0-dev.17 + +- Error thrown by `.assert` or `out.throw()` is now an instance of [AggregateError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError), with the cause being an `ArkErrors` array. + ## 2.0.0-dev.16 - Fix an incorrect return value on pipe sequences like the following: diff --git a/ark/type/__tests__/traverse.test.ts b/ark/type/__tests__/traverse.test.ts index 9d5531776b..7a5829b15b 100644 --- a/ark/type/__tests__/traverse.test.ts +++ b/ark/type/__tests__/traverse.test.ts @@ -146,6 +146,24 @@ contextualize(() => { ) }) + it("homepage example", () => { + const user = type({ + name: "string", + luckyNumbers: "(number | bigint)[]", + "isAdmin?": "boolean | null" + }) + + const out = user({ + luckyNumbers: [31, "255", 1337n], + isAdmin: 1 + }) + + attest(out.toString()) + .snap(`luckyNumbers[1] must be a bigint or a number (was string) +name must be a string (was missing) +isAdmin must be false, null or true (was 1)`) + }) + it("relative path", () => { const signup = type({ email: "email", diff --git a/ark/type/__tests__/type.test.ts b/ark/type/__tests__/type.test.ts index 45b08aa851..408b32864e 100644 --- a/ark/type/__tests__/type.test.ts +++ b/ark/type/__tests__/type.test.ts @@ -27,9 +27,18 @@ contextualize(() => { const result = t("invalid") attest(result instanceof type.errors && result.throw()) } catch (e) { - attest(e instanceof type.errors).equals(true) + attest(e instanceof AggregateError).equals(true) + attest((e as AggregateError).errors instanceof type.errors) return } throw new AssertionError({ message: "Expected to throw" }) }) + + it("assert", () => { + const t = type({ a: "string" }) + attest(t.assert({ a: "1" })).equals({ a: "1" }) + attest(() => t.assert({ a: 1 })).throws.snap( + "AggregateError: a must be a string (was number)" + ) + }) }) diff --git a/ark/util/tsconfig.base.json b/ark/util/tsconfig.base.json index 4db01a8762..f5e3d32853 100644 --- a/ark/util/tsconfig.base.json +++ b/ark/util/tsconfig.base.json @@ -12,6 +12,7 @@ "esModuleInterop": true, "resolveJsonModule": true, "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, "stripInternal": true } }