diff --git a/.cursor/rules/arktype.mdc b/.cursor/rules/arktype.mdc new file mode 100644 index 000000000..9f54c824a --- /dev/null +++ b/.cursor/rules/arktype.mdc @@ -0,0 +1,113 @@ +--- +description: When working with ArkType for schema validation. Covers common patterns, pitfalls, and idiomatic usage. +globs: "**/*.ts" +alwaysApply: false +--- + +# ArkType Usage Guidelines + +## Optional Keys + +```ts +// ❌ WRONG +type({ name: "string | undefined" }) + +// ✅ CORRECT — append ? to the key +type({ "name?": "string" }) +``` + +## Schema References + +**In objects** — pass schemas directly: +```ts +const Inner = type({ foo: "string" }) +const Outer = type({ inner: Inner }) +``` + +**In records** — use index signature syntax: +```ts +// ❌ WRONG: "Record" +// ✅ CORRECT: +type({ "[string]": Schema }) +``` + +**In arrays** — use `.array()`: +```ts +// ❌ WRONG: type([Schema]) — creates a 1-element tuple +// ✅ CORRECT: +Schema.array() +``` + +## Type Inference + +```ts +type User = typeof UserSchema.infer +``` + +## Type Declaration + +Match a schema to an existing TypeScript type: +```ts +const Schema = type.declare().type({ + items: type.string.array().readonly() // readonly string[] +}) +``` + +## Error Handling + +```ts +const out = Schema(data) +if (out instanceof type.errors) { + console.error(out.summary) +} +``` + +## Syntax Kinds + +Three equivalent ways to define types: + +```ts +// string expression +type("string | number") + +// tuple expression +type(["string", "|", "number"]) + +// chained +type("string").or("number") +``` + +## Keywords Pattern + +Keywords follow `typescriptType.constraint.subconstraint`: + +- `string.email`, `string.uuid`, `string.url` +- `number.integer`, `number.safe`, `number.epoch` +- `string.trim`, `string.json.parse`, `string.date.parse` (morphs) + +## Operators + +```ts +type("string | number") // union +type("string & /@pattern/") // intersection +type("number > 0") // exclusive min +type("number >= 0") // inclusive min +type("string <= 255") // max length +type({ name: "string = 'default'" }) // default value +``` + +## Morphs + +```ts +type("string").to("number.integer") // validated transform +type("string").pipe(s => s.trim()) // custom transform +type("string.json.parse") // built-in parse morph +``` + +## Common Mistakes + +1. `type([X])` creates a tuple, not an array — use `X.array()` +2. `"string | undefined"` for optional — use `"key?": "string"` +3. `"Record"` — use `type({ "[string]": T })` +4. Re-validating after `type()` passes — trust ArkType's output +5. `typeof Schema.t` — use `typeof Schema.infer` for output type diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 000000000..681311eb9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..ce96b9b19 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,85 @@ +# ArkType — Agent Context + +ArkType is TypeScript's 1:1 validator, optimized from editor to runtime. It parses TypeScript-like string syntax at runtime — this is unique among validation libraries. + +## Quick Start + +```bash +pnpm i && pnpm build # install and build all packages +pnpm prChecks # lint + build + test (run before PRs) +pnpm testTyped --skipTypes # run tests without type checking +``` + +## Monorepo Structure + +- `ark/type/` — main validation library (`arktype` on npm) +- `ark/schema/` — schema layer (constraint nodes, refinements, scopes) +- `ark/util/` — shared utilities +- `ark/docs/` — documentation site (Fumadocs/Next.js) +- `ark/json-schema/` — JSON Schema conversion +- `ark/regex/` — ArkRegex type-safe regex +- `ark/attest/` — custom assertion/test library +- `ark/fast-check/` — property testing integration + +## Key Patterns + +**String keywords** follow `typescriptType.constraint.subconstraint`: + +- `string.email`, `string.uuid`, `string.url`, `string.hex` +- `number.integer`, `number.safe`, `number.epoch` +- Morphs: `string.trim`, `string.json.parse`, `string.date.parse` + +**Adding a regex string validator:** + +```ts +const myValidator = regexStringNode(/^pattern$/, "description") +``` + +Register in `Scope.module()` in `ark/type/keywords/string.ts` and add to the namespace `$` type. + +**Adding a number keyword:** +Use `rootSchema()` with `domain`, `min`/`max`, or `predicate` constraints. See `number.safe` in `ark/type/keywords/number.ts`. + +## Code Style + +- Tabs, no semicolons, no trailing commas (`prettier` enforced) +- `experimentalTernaries: true` — `?` at end of line, `:` on next line +- `arrowParens: "avoid"` — `x => x` not `(x) => x` +- Tests use `attest` (custom lib) with `.snap()` for snapshots + +## ArkType Syntax Cheat Sheet + +```ts +// optional keys — append ? to the key name +type({ "name?": "string" }) + +// arrays — use .array(), NOT type([Schema]) +Schema.array() + +// records — use index signature syntax +type({ "[string]": ValueSchema }) + +// type inference +type User = typeof UserSchema.infer + +// error handling +const out = Schema(data) +if (out instanceof type.errors) { + console.error(out.summary) +} + +// three syntax kinds (equivalent): +type("string | number") // string expression +type(["string", "|", "number"]) // tuple expression +type("string").or("number") // chained +``` + +## Common Gotchas + +- `type([X])` creates a 1-element **tuple**, not an array — use `X.array()` +- `"string | undefined"` for optional — use `"key?": "string"` instead +- `"Record"` doesn't work — use `type({ "[string]": T })` +- `safeParse()` doesn't exist — call the type directly: `const out = Schema(data)` +- `regexStringNode()` patterns must be anchored with `^`/`$` + + diff --git a/ark/docs/content/docs/cheat-sheet.mdx b/ark/docs/content/docs/cheat-sheet.mdx new file mode 100644 index 000000000..37f149583 --- /dev/null +++ b/ark/docs/content/docs/cheat-sheet.mdx @@ -0,0 +1,166 @@ +--- +title: Cheat Sheet +--- + +A quick reference for ArkType's most common patterns. For full docs, explore the sidebar. + +## Syntax Kinds + +ArkType offers three ways to define the same type. Use whichever fits your context. + +```ts +import { type } from "arktype" + +// string expression — most concise +const StringSyntax = type("string | number") + +// tuple expression — embed non-string operands +const TupleSyntax = type(["string", "|", "number"]) + +// chained — compose existing types +const Chained = type("string").or("number") +``` + +## Keywords + +Keywords follow a `typescriptType.constraint.subconstraint` pattern: + +```ts +// string keywords +type("string.email") // email format +type("string.uuid") // UUID format +type("string.url") // parsable URL +type("string.trim") // morph: trims whitespace +type("string.json.parse") // morph: string → parsed JSON +type("string.date.parse") // morph: string → Date + +// number keywords +type("number.integer") // whole numbers +type("number.safe") // within safe integer range +type("number.epoch") // valid Unix timestamp +``` + +## Operators + +```ts +type("string | number") // union +type("string & /@foo/") // intersection +type("number > 0") // exclusive min +type("number >= 0") // inclusive min +type("string <= 255") // max length + +// defaults — valid inside objects (key becomes implicitly optional) +type({ email: "string.email = 'n/a'" }) +``` + +## Objects + +```ts +const User = type({ + name: "string", + "email?": "string.email", // optional key — use '?' suffix in the key + age: "number >= 0 = 0" // default value — implicitly optional +}) + +// index signatures (Record types) +const Env = type({ + "[string]": "string" // Record +}) + +// strip undeclared keys +const Strict = User.onUndeclaredKey("delete") +``` + +## Arrays and Tuples + +```ts +const Strings = type("string[]") // string expression +const Numbers = type.number.array() // chained + +// tuples — fixed length and positional types +const Pair = type(["string", "number"]) + +// GOTCHA: type([X]) is a 1-element tuple, NOT an array +const Item = type({ id: "string" }) +const Wrong = type([Item]) // tuple of exactly [Item] +const Right = Item.array() // Item[] + +// readonly +const Ids = type("string[]").readonly() // readonly string[] +``` + +## Composition + +```ts +const Device = type({ + platform: "'android' | 'ios'", + "version?": "string" +}) + +// reference in another type — just pass it directly +const User = type({ + name: "string", + device: Device +}) + +// fluent composition +const Admin = User.and({ role: "'admin'" }) +const Guest = type({ name: "string" }).or({ token: "string" }) +``` + +## Morphs and Pipes + +```ts +// .to() — validated input → validated output +const ParsedInt = type("string").to("number.integer") + +// .pipe() — validated input → transform function +const ToUpper = type("string").pipe(s => s.toUpperCase()) + +// built-in parse morphs +const ParseDate = type("string.date.parse") // string → Date +const ParseJson = type("string.json.parse") // string → object +const Trimmed = type("string.trim") // string → trimmed string +``` + +## Type Inference + +```ts +const User = type({ + name: "string", + "age?": "number" +}) + +// extract the TypeScript type +type User = typeof User.infer +// { name: string; age?: number } +``` + +## Error Handling + +```ts +const User = type({ + name: "string", + "age?": "number" +}) + +const out = User({ name: "Alan", age: "not a number" }) + +if (out instanceof type.errors) { + // ArkErrors — array-like with .summary + console.error(out.summary) +} else { + // out is typed as { name: string; age?: number } + console.log(out.name) +} +``` + +## Common Gotchas + +| Mistake | Fix | +| ------------------------------------------ | ----------------------------------------------------------- | +| `name: "string \| undefined"` for optional | Use `"name?": "string"` — append `?` to the **key** | +| `type([Schema])` for arrays | Use `Schema.array()` — brackets create tuples | +| `"Record"` for records | Use `type({ "[string]": T })` — index signature syntax | +| Manually re-validating after `type()` | Trust ArkType's output — it already validated the structure | +| `type User = typeof Schema.t` | Use `typeof Schema.infer` for the output type | diff --git a/ark/docs/content/docs/meta.json b/ark/docs/content/docs/meta.json index b2f7ab553..bdcbcb575 100644 --- a/ark/docs/content/docs/meta.json +++ b/ark/docs/content/docs/meta.json @@ -1,6 +1,7 @@ { "pages": [ "intro", + "cheat-sheet", "primitives", "objects", "keywords",