Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions .cursor/rules/arktype.mdc
Original file line number Diff line number Diff line change
@@ -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<string, Schema>"
// ✅ 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<MyType>().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<string, T>"` — 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
1 change: 1 addition & 0 deletions AGENTS.md
85 changes: 85 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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<string, T>"` 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 `^`/`$`

<!-- Keep in sync: .cursor/rules/arktype.mdc, ark/docs/content/docs/cheat-sheet.mdx -->
166 changes: 166 additions & 0 deletions ark/docs/content/docs/cheat-sheet.mdx
Original file line number Diff line number Diff line change
@@ -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<string, string>
})

// 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<string, T>"` 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 |
1 change: 1 addition & 0 deletions ark/docs/content/docs/meta.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"pages": [
"intro",
"cheat-sheet",
"primitives",
"objects",
"keywords",
Expand Down
Loading