# ArkType > TypeScript's 1:1 validator # Setup ## Installation ```bash npm install arktype ``` You'll also need... - TypeScript version `>=5.1`. - A `package.json` with `"type": "module"` (or an environment that supports ESM imports) - A `tsconfig.json` with... - [`strict`](https://www.typescriptlang.org/tsconfig#strict) or [`strictNullChecks`](https://www.typescriptlang.org/tsconfig#strictNullChecks) (**required**) - [`skipLibCheck`](https://www.typescriptlang.org/tsconfig#skipLibCheck) (strongly recommended, see [FAQ](https://arktype.io/docs/faq#why-do-i-see-type-errors-in-an-arktype-package-in-node_modules)) - [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) (recommended) ## VSCode ### Settings To take advantage of all of ArkType's autocomplete capabilities, you'll need to add the following to your workspace settings at `.vscode/settings.json`: ```json // 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) [ArkDark](https://marketplace.visualstudio.com/items?itemName=arktypeio.arkdark) provides the embedded syntax highlighting you'll see throughout the docs. Without it, your definitions can still feel like a natural extension of the language. With it, you'll forget there was ever a boundary in the first place. ## JetBrains IDEs ### Extension (optional) [ArkType](https://plugins.jetbrains.com/plugin/27099-arktype) provides the embedded syntax highlighting you are familiar with for typescript types. ## Other editors If you're using a different editor, we'd love [help adding support](https://github.com/arktypeio/arktype/issues/989). In the meantime, don't worry- ArkType still offers best-in-class DX anywhere TypeScript is supported. # Your First Type If you already know TypeScript, congratulations- you just learned most of ArkType's syntax πŸŽ‰ ## Define ```ts import { type } from "arktype" const User = type({ name: "string", platform: "'android' | 'ios'", "versions?": "(number | string)[]" }) // extract the type if needed type User = typeof User.infer ``` If you make a mistake, don't worry- every definition gets the autocomplete and validation you're used to from your editor, all within TypeScript's type system. > **Will ArkType crash my TypeScript server?** > > Thousands of hours of optimization have gone into making validating native > type syntax not just feasible, but often much faster than alternatives ⚑ ## Compose Suppose we want to move `platform` and `versions` from our original type to a new `device` property. ```ts const User = type({ name: "string", // nested definitions don't need to be wrapped device: { platform: "'android' | 'ios'", "versions?": "(number | string)[]" } }) ``` To decouple `device` from `User`, just move it to its own type and reference it. ```ts const Device = type({ platform: "'android' | 'ios'", "versions?": "(number | string)[]" }) const User = type({ name: "string", device: Device }) ``` ## Validate At runtime, we can pass `unknown` data to our type and get back either a validated `User` or an array of clear, customizable errors with a root `summary`. ```ts const out = User({ name: "Alan Turing", device: { platform: "enigma", versions: [0, "1", 0n] } }) if (out instanceof type.errors) { // hover out.summary to see validation errors console.error(out.summary) } else { // hover out to see your validated data console.log(`Hello, ${out.name}`) } ``` And that's it! You now know how to define a `Type` and use it to check your data at runtime. Next, we'll take a look at how ArkType extends TypeScript's type system to handle runtime constraints like `maxLength` and `pattern`. # Adding Constraints TypeScript is extremely versatile for representing types like `string` or `number`, but what about `email` or `integer less than 100`? In ArkType, conditions that narrow a type beyond its **basis** are called **constraints**. Constraints are a first-class citizen of ArkType. They are fully composable with TypeScript's built-in operators and governed by the same underlying principles of set-theory. ## Define Let's create a new `contact` Type that enforces our example constraints. ```ts const Contact = type({ // many common constraints are available as built-in keywords email: "string.email", // others can be written as type-safe expressions score: "number.integer < 100" }) // if you need the TS type, just infer it out as normal type Contact = typeof Contact.infer ``` ## Compose Imagine we want to define a new Type representing a non-empty list of `Contact`. While the expression syntax we've been using is ideal for creating new types, chaining is a great way to refine or transform existing ones. ```ts const _Contact = type({ email: "string.email", score: "number.integer < 100" }) type _Contact = typeof _Contact.t interface Contact extends _Contact {} export const Contact: type = _Contact const Contacts = Contact.array().atLeastLength(1) ``` ## Narrow Structured constraints like divisors and ranges will only take us so far. Luckily, they integrate seamlessly with whatever custom validation logic you need. ```ts // there's no "not divisible" expression - need to narrow const Odd = type("number").narrow((n, ctx) => // if even, add a customizable error and return false n % 2 === 0 ? ctx.mustBe("odd") : true ) const FavoriteNumbers = type({ even: "number % 2", odd: Odd }) const out = FavoriteNumbers({ even: 7, odd: 8 }) if (out instanceof type.errors) { // hover summary to see validation errors console.error(out.summary) } else { console.log(out.odd) } ``` You now know how to refine your types to enforce additional constraints at runtime. But what if once your input is fully validated, you still need to make some adjustments before it's ready to use? The final section of intro will cover **morphs**, an extremely powerful tool for composing and transforming Types. # Morphs & More Sometimes, data at the boundaries of your code requires more than validation before it's ready to use. **Morphs** allow you to arbitrarily transform the shape and format of your data. Morphs can be **piped** before, after or between validators and even chained to other morphs. ```ts // Hover to see the type-level representation const parseJson = type("string").pipe((s): object => JSON.parse(s)) // object: { ark: "type" } const out = parseJson('{ "ark": "type" }') // ArkErrors: must be a string (was object) const badOut = parseJson(out) ``` This is a good start, but there are still a couple major issues with our morph. What happens if we pass a string that isn't valid JSON? ```ts // Uncaught SyntaxError: Expected property name ☠️ const badOut = parseJson('{ unquoted: "keys" }') ``` Despite what `JSON.parse` might have you believe, throwing exceptions and returning `any` are not very good ways to parse a string. By default, ArkType assumes that if one of your morphs or narrows throws, you intend to crash. If you do happen to find yourself at the mercy of an unsafe API, you might consider wrapping your function body in a `try...catch`. Luckily, there is a built-in API for wrapping `pipe`d functions you don't trust: ```ts const parseJson = type("string").pipe.try((s): object => JSON.parse(s)) // Now returns an introspectable error instead of crashing πŸŽ‰ const badOut = parseJson('{ unquoted: "keys" }') const out = parseJson('{ "ark": "type" }') if (out instanceof type.errors) out.throw() // Unfortunately, a validated `object` still isn't very useful... else console.log(out) ``` The best part about `pipe` is that since any `Type` is root-invokable, `Type`s themselves _are_ already morphs! This means validating out parsed output is as easy as adding another pipe: ```ts const parseJson = type("string").pipe.try( (s): object => JSON.parse(s), type({ name: "string", version: "string.semver" }) ) const out = parseJson('{ "name": "arktype", "version": "2.0.0" }') if (!(out instanceof type.errors)) { // Logs "arktype:2.0.0" console.log(`${out.name}:${out.version}`) } ``` At this point, our implementation is starting to look pretty clean, but in many cases like this one, we can skip straight to the punch line with one of ArkType's many built-in aliases for validation and parsing, `string.json.parse`: ```ts // .to is a sugared .pipe for a single parsed output validator const parseJson = type("string.json.parse").to({ name: "string", version: "string.semver" }) const out = parseJson('{ "name": true, "version": "v2.0.0" }') if (out instanceof type.errors) { // hover out.summary to see the default error message console.error(out.summary) } ``` If you've made it this far, congratulations! You should have all the fundamental intuitions you need to bring your types to runtime β›΅ Our remaining docs will help you understand the trade offs between ArkType's most important APIs so that no matter the application, you can find a solution that feels great to write, great to read, and great to run. # 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"` with `T` a variable | Use `type.Record("string", T)` or `{ "[string]": T }` | | 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 | # Agents > Help Claude Code, Codex, Cursor and other coding agents write ArkType ## Skill The ArkType skill tells agents when to reach for ArkType, how its syntax maps to TypeScript, and which mistakes to avoid. Add it to your project with: ```bash npx skills add arktypeio/arktype ``` Releases after 2.2.8 also include the skill in the `arktype` package at `skills/arktype/SKILL.md`, so tools like [skills-npm](https://github.com/antfu/skills-npm) can link the copy that matches your installed version. ## Docs as Markdown Every page of these docs is available as Markdown by adding `.md` to its URL, e.g. [/docs/intro/setup.md](https://arktype.io/docs/intro/setup.md). The buttons under each page's title copy it or open it in Claude, ChatGPT or Cursor. [/llms.txt](https://arktype.io/llms.txt) has every ArkType page in one file, in the order of the sidebar. Attest's docs are at [/docs/attest/llms.txt](https://arktype.io/docs/attest/llms.txt). ## AGENTS.md If your project has an `AGENTS.md` or `CLAUDE.md`, a line like this keeps agents on ArkType when they add validation: ``` Validate untrusted input with ArkType. Docs: https://arktype.io/llms.txt ``` # Primitives ## string ### keywords The following keywords can be referenced in any definition, e.g.: ```ts const Email = type("string.email") const User = type({ data: "string.json.parse", ids: "string.uuid.v4[]" }) ``` | Alias | Description | | --- | --- | | `string` | a string | | `string.alpha` | only letters | | `string.alphanumeric` | only letters and digits 0-9 | | `string.base58` | base58-encoded | | `string.base64` | base64-encoded | | `string.base64.url` | base64url-encoded | | `string.capitalize` | a morph from a string to capitalized | | `string.capitalize.preformatted` | capitalized | | `string.creditCard` | a credit card number and a credit card number | | `string.date` | a string and a parsable date | | `string.date.epoch` | an integer string representing a safe Unix timestamp | | `string.date.epoch.parse` | a morph from an integer string representing a safe Unix timestamp to a Date | | `string.date.iso` | an ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ) date | | `string.date.iso.parse` | a morph from an ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ) date to a Date | | `string.date.parse` | a morph from a string and a parsable date to a Date | | `string.digits` | only digits 0-9 | | `string.email` | an email address | | `string.hex` | hex characters only | | `string.integer` | a well-formed integer string | | `string.integer.parse` | a morph from a well-formed integer string to an integer | | `string.ip` | an IP address | | `string.ip.v4` | an IPv4 address | | `string.ip.v6` | an IPv6 address | | `string.json` | a JSON string | | `string.json.parse` | safe JSON string parser | | `string.lower` | a morph from a string to only lowercase letters | | `string.lower.preformatted` | only lowercase letters | | `string.normalize` | a morph from a string to NFC-normalized unicode | | `string.normalize.NFC` | a morph from a string to NFC-normalized unicode | | `string.normalize.NFC.preformatted` | NFC-normalized unicode | | `string.normalize.NFD` | a morph from a string to NFD-normalized unicode | | `string.normalize.NFD.preformatted` | NFD-normalized unicode | | `string.normalize.NFKC` | a morph from a string to NFKC-normalized unicode | | `string.normalize.NFKC.preformatted` | NFKC-normalized unicode | | `string.normalize.NFKD` | a morph from a string to NFKD-normalized unicode | | `string.normalize.NFKD.preformatted` | NFKD-normalized unicode | | `string.numeric` | a well-formed numeric string | | `string.numeric.parse` | a morph from a well-formed numeric string to a number | | `string.regex` | a string and a regex pattern | | `string.semver` | a semantic version (see https://semver.org/) | | `string.trim` | a morph from a string to trimmed | | `string.trim.preformatted` | trimmed | | `string.upper` | a morph from a string to only uppercase letters | | `string.upper.preformatted` | only uppercase letters | | `string.url` | a string and a URL string | | `string.url.parse` | a morph from a string and a URL string to a URL instance | | `string.uuid` | a UUID | | `string.uuid.v1` | a UUIDv1 | | `string.uuid.v2` | a UUIDv2 | | `string.uuid.v3` | a UUIDv3 | | `string.uuid.v4` | a UUIDv4 | | `string.uuid.v5` | a UUIDv5 | | `string.uuid.v6` | a UUIDv6 | | `string.uuid.v7` | a UUIDv7 | | `string.uuid.v8` | a UUIDv8 | ### 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$/ }) ``` Regex literals now carry full type inference via [arkregex](https://arktype.io/docs/blog/arkregex): ```ts const Hex = type("/^[0-9a-fA-F]+$/") // Type const Semver = type("/^(\\d+)\\.(\\d+)\\.(\\d+)$/") // Type<`${number}.${number}.${number}`> ``` #### e(x)ec mode Prefix a regex literal with `x` to parse capture groups at runtime, fully typed: ```ts const User = type({ birthday: "x/^(?\\d{2})-(?\\d{2})-(?\\d{4})$/" }) const data = User.assert({ birthday: "05-21-1993" }) // fully type-safe data.birthday.groups.month // "05" data.birthday.groups.day // "21" data.birthday.groups.year // "1993" ``` For the standalone package (no ArkType required), see the full [arkregex announcement](https://arktype.io/docs/blog/arkregex). ### lengths Constrain a string with an inclusive or exclusive min or max length. **string** ```ts const Bounded = type({ nonEmpty: "string > 0", atLeastLength3: "string.alphanumeric >= 3", lessThanLength10: "string < 10", atMostLength5: "string <= 5" }) ``` **fluent** ```ts const Bounded = type({ nonEmpty: type.string.moreThanLength(0), atLeastLength3: type.keywords.string.alphanumeric.atLeastLength(3), lessThanLength10: type.string.lessThanLength(10), atMostLength5: type.string.atMostLength(5) }) ``` Range expressions allow you to specify both a min and max length and use the same syntax for exclusivity. **string** ```ts const Range = type({ nonEmptyAtMostLength10: "0 < string <= 10", integerStringWith2To5Digits: "2 <= string.integer < 6" }) ``` **fluent** ```ts const Range = type({ nonEmptyAtMostLength10: type.string.moreThanLength(0).atMostLength(10), integerStringWith2To5Digits: type.keywords.string.integer.root .atLeastLength(2) .lessThanLength(6) }) ``` ## number ### keywords The following keywords can be referenced in any definition, e.g.: ```ts const User = type({ createdAt: "number.epoch", age: "number.integer >= 0" }) ``` | Alias | Description | | --- | --- | | `number` | a number | | `number.Infinity` | Infinity | | `number.NaN` | NaN | | `number.NegativeInfinity` | -Infinity | | `number.epoch` | an integer representing a safe Unix timestamp | | `number.integer` | an integer | | `number.safe` | at least -9007199254740991 and at most 9007199254740991 | ### literals ```ts const Literals = type({ number: "1337" }) ``` ### ranges Constrain a number with an inclusive or exclusive min or max. **string** ```ts const Bounded = type({ positive: "number > 0", atLeast3: "number.integer >= 3", lessThanPi: "number < 3.14159", atMost5: "number <= 5" }) ``` **fluent** ```ts const Bounded = type({ positive: type.number.moreThan(0), atLeast3: type.keywords.number.integer.atLeast(3), lessThanPi: type.number.lessThan(3.14159), atMost5: type.number.atMost(5) }) ``` Range expressions allow you to specify both a min and max and use the same syntax for exclusivity. **string** ```ts const Range = type({ positiveAtMostE: "0 < number <= 2.71828", evenNumberAbsoluteValueLessThan50: "-50 < (number % 2) < 50" }) ``` **fluent** ```ts const Range = type({ positiveAtMostE: type.number.moreThan(0).atMost(2.71828), evenNumberAbsoluteValueLessThan50: type.number .divisibleBy(2) .moreThan(-50) .lessThan(50) }) ``` ### divisors Constrain a `number` to a multiple of the specified integer. **string** ```ts const Evens = type({ key: "number % 2" }) ``` **fluent** ```ts const Evens = type({ key: type.number.divisibleBy(2) }) ``` ## bigint To allow any `bigint` value, use the `"bigint"` keyword. **string** ```ts const Bigints = type({ foo: "bigint" }) ``` **fluent** ```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](https://arktype.io/docs/primitives#number-literals) to make it a `bigint`. ```ts const Literals = type({ bigint: "1337n" }) ``` You may also use a [unit expression](https://arktype.io/docs/expressions#unit) to define `bigint` literals. ## symbol To allow any `symbol` value, use the `"symbol"` keyword. **string** ```ts const Symbols = type({ key: "symbol" }) ``` **fluent** ```ts const Symbols = type({ key: type.symbol }) ``` To reference a specific symbol in your definition, use a [unit expression](https://arktype.io/docs/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](https://arktype.io/docs/objects#properties). ## boolean To allow `true` or `false`, use the `"boolean"` keyword. **string** ```ts const Booleans = type({ key: "boolean" }) ``` **fluent** ```ts const Booleans = type({ key: type.boolean }) ``` ### literals To require a specific boolean value, use the corresponding literal. **string** ```ts const Booleans = type({ a: "true", b: "false", // equivalent to the "boolean" keyword c: "true | false" }) ``` **fluent** ```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](https://arktype.io/docs/expressions#union). **string** ```ts const MyObj = type({ foo: "number | null" }) ``` **fluent** ```ts const MyObj = type({ foo: type.number.or(type.null) }) ``` ## undefined The `"undefined"` keyword can be used to allow the exact value `undefined`, generally as part of a [union](https://arktype.io/docs/expressions#union). **string** ```ts const MyObj = type({ requiredKey: "number | undefined", "optionalKey?": "number | undefined" }) ``` **fluent** ```ts const MyObj = type({ requiredKey: type.number.or(type.undefined), optionalKey: type.number.or(type.undefined).optional() }) ``` > **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({}) > ``` > >
# Objects ## properties Objects definitions can include any combination of required, optional, defaultable named properties and index signatures. ### required **string** ```ts const symbolicKey = Symbol() const MyObj = type({ requiredKey: "string", // Nested definitions don't require additional `type` calls! [symbolicKey]: { nested: "unknown" } }) ``` **fluent** ```ts const symbolicKey = Symbol() const MyObj = type({ requiredKey: type.string, // Nested definitions don't require additional `type` calls! [symbolicKey]: { nested: type.unknown } }) ``` ### optional **string** ```ts const symbolicKey = Symbol() const MyObj = type({ "optionalKey?": "number[]", [symbolicKey]: "string?" }) ``` **fluent** ```ts const symbolicKey = Symbol() const MyObj = type({ optionalKey: type.number.array().optional(), [symbolicKey]: type.string.optional() }) ``` **tuple** ```ts const symbolicKey = Symbol() const MyObj = type({ optionalKey: [{ type: "'script'" }, "?"], [symbolicKey]: [{ ark: "'type'" }, "?"] }) ``` > **Should I use key or value syntax for optionality?** > > Optionality can either be expressed on the key or the corresponding value. > > We recommend using the key syntax by default because... > > - it mirrors TypeScript > - it better reflects a _key presence_ constraint that has no effect on allowed values > > However, there are a few reasons you might want to use a value-embedded syntax: > > 1. The key is a symbol (makes key-embedded syntax impossible) > 2. You want editor features like JSDoc and go-to-definition for the key (can't work if the key name changes) > 3. You really hate having to quote key names > **Optional properties cannot be present with the value undefined** > > In TypeScript, there is a setting called [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig#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. Though not recommended as a long-term solution, you may also [globally configure `exactOptionalPropertyTypes`](https://arktype.io/docs/configuration#exactoptionalpropertytypes) to `false`. > >
> 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 }) > ``` > >
### defaultable **string** ```ts const MyObj = type({ defaultableKey: "boolean = false" }) ``` **fluent** ```ts const MyObj = type({ defaultableKey: type.boolean.default(false) }) ``` **tuple** ```ts const MyObj = type({ defaultableKey: ["boolean", "=", false] }) ``` An empty array or object can be defaulted directly in string syntax. A shared reference would leak mutations from one traversal into the next, so `= []` and `= {}` assign a new array or object each time the default is applied- exactly like the thunk form they replace. ```ts const MyObj = type({ // each parsed value gets its own array values: "string[] = []", meta: "object = {}" }) ``` > **Optional and default only work within objects and tuples!** > > Unlike e.g. `number.array()`, `number.optional()` and `number.default(0)` don't return a new `Type`, but rather a tuple definition like `[Type, "?"]` or `[Type, "=", 0]`. > > This reflects the fact that in ArkType's type system, optionality and defaultability are only meaningful in reference to a property. Attempting to create an optional or defaultable value outside an object like `type("string?")` will result in a `ParseError`. > > To create a `Type` accepting `string` or `undefined`, use a union like `type("string | undefined")`. > > To have it transform `undefined` to an empty string, use an explicit morph like: > > ```ts > const FallbackString = type("string | undefined").pipe(v => v ?? "") > ``` ### index **string** ```ts const MyObj = type({ // index signatures do not require a label "[string]": "number.integer", // arbitrary string or symbolic expressions are allowed "[string | symbol]": "number" }) ``` ### undeclared TypeScript's structural type system explicitly allows assigning objects with additional keys so long as all declared constraints are satisfied. ArkType mirrors this behavior by default because generally... - Existing objects can be reused more often. - Validation is much more efficient if you don't need to check for undeclared keys. - Extra properties don't usually matter as long as those you've declared are satisfied. However, sometimes the way you're using the object would make undeclared properties problematic. Even though they can't be reflected by TypeScript ([_yet_- please +1 the issue!](https://github.com/microsoft/TypeScript/issues/12936#issuecomment-1854411301)), ArkType _does_ support rejection or deletion of undeclared keys. This behavior can be defined for individual objects using the syntax below or [via configuration](https://arktype.io/docs/configuration#onundeclaredkey) if you want to change the default across all objects. **string** ```ts // fail if any key other than "onlyAllowedKey" is present const MyClosedObject = type({ "+": "reject", onlyAllowedKey: "string" }) // delete all non-symbolic keys other than "onlyPreservedStringKey" const MyStrippedObject = type({ "+": "delete", "[symbol]": "unknown", onlyPreservedStringKey: "string" }) // allow and preserve undeclared keys (the default behavior) const MyOpenObject = type({ // only specify "ignore" if you explicitly configured the default elsewhere "+": "ignore", nonexclusiveKey: "number" }) ``` **fluent** ```ts // fail if any key other than "onlyAllowedKey" is present const MyClosedObject = type({ onlyAllowedKey: "string" }).onUndeclaredKey("reject") // delete all non-symbolic keys other than "onlyPreservedStringKey" const MyStrippedObject = type({ "[symbol]": "unknown", onlyPreservedStringKey: "string" }).onUndeclaredKey("delete") // allow and preserve undeclared keys (the default behavior) const MyOpenObject = type({ nonexclusiveKey: "number" // only specify "ignore" if you explicitly configured the default elsewhere }).onUndeclaredKey("ignore") // there is also a method for altering nested objects recursively const MyDeeplyStrippedObject = type({ preserved: "string", nested: { preserved: "string" } }).onDeepUndeclaredKey("delete") ``` > **Prefer in-object syntax where possible** > > Certain methods like `.onUndeclaredKey` or `.configure` require a full traversal and transformation of the node created by the initial `type` call. > >
> **Learn to recognize when chaining creates unnecessary overhead** > > Though they can be convenient if you need both variants of the type, most of the time you incur a significant performance cost instantiating your Type compared to the embedded syntax. > > Though how a Type is defined will never affect validation performance, depending on your sensitivity to initialization, you may want to avoid chained methods that transform rather than compose their base type. Methods like `.or` and `.pipe` that create new `Types` that directly reference the original incur no such overhead, so feel free to use whichever syntax is more convenient for those operations. > >
### merge **merge** allows you to extend an object with new properties or replace existing ones like the `...` operator [in JS](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax). When applied to two distinct (i.e. non-overlapping) sets of properties, it is a more efficient equivalent of [intersection](https://arktype.io/docs/expressions#intersection). However, if a key appears in both the base and merged objects, the base value will be discarded in favor of the merged rather than recursively intersected. Spreading bypasses a lot of the behavioral complexity and computational overhead of an intersection and should be the preferred method of combining property sets. Like ArkType's other [structural transformations](https://arktype.io/docs/objects#properties-structural), `merge` returns a new object built from the properties it extracts, so a root narrow or metadata is not carried over. **fluent** ```ts const zildjian = Symbol() const Base = type({ "[string]": "number", foo: "0", [zildjian]: "true" }) // hover to see the inferred result const chainedResult = Base.merge({ "[string]": "bigint", "foo?": "1n" }) ``` **string** ```ts const User = type({ isAdmin: "false", name: "string" }) // hover to see the newly merged object const Admin = type({ "...": User, // in an intersection, non-overlapping values at isAdmin would result in a ParseError isAdmin: "true", permissions: "string[]" }) ``` **generic** ```ts const types = type.module({ base: { "foo?": "0", "bar?": "0" }, merged: { bar: "1", "baz?": "1" }, result: "Merge" }) // hover to see the inferred result type Result = typeof types.result.infer ``` #### n-ary To merge many objects directly, `type.merge` will avoid the need to chain or compose many binary expressions: ```ts const zildjian = Symbol() const Base = type({ "[string]": "number", foo: "0", [zildjian]: "true" }) // accepts ...objectDefinitions const functionResult = type.merge( Base, { "[string]": "bigint", "foo?": "1n" }, { includeThisPropAlso: "true" } ) ``` ### keyof Like in TypeScript, the `keyof` operator extracts the keys of an object as a union: **fluent** ```ts const UsedCar = type({ originallyPurchased: "string.date", remainingWheels: "number" }) const UsedCarKey = UsedCar.keyof() type UsedCarKey = typeof UsedCarKey.infer ``` **string** ```ts const types = type.module({ UsedCar: { originallyPurchased: "string.date", remainingWheels: "number" }, UsedCarKey: "keyof UsedCar" }) type UsedCarKey = typeof types.UsedCarKey.infer ``` **tuple** ```ts const UsedCar = type({ originallyPurchased: "string.date", remainingWheels: "number" }) const UsedCarKey = type(["keyof", UsedCar]) type UsedCarKey = typeof UsedCarKey.infer ``` **args** ```ts const UsedCar = type({ originallyPurchased: "string.date", remainingWheels: "number" }) const UsedCarKey = type("keyof", UsedCar) type UsedCarKey = typeof UsedCarKey.infer ``` Also like in TypeScript, if an object includes an index signature like `[string]` alongside named properties, the union from `keyof` will reduce to `string`: ```ts const RecordWithSpecialKeys = type({ "[string]": "unknown", verySpecialKey: "0 < number <= 3.14159", moderatelySpecialKey: "-9.51413 <= number < 0" }) // in a union with the `string` index signature, string literals // "verySpecialKey" and "moderatelySpecialKey" are redundant and will be pruned const Key = RecordWithSpecialKeys.keyof() // key is identical to the base `string` Type console.log(Key.equals("string")) ``` > **ArkType's `keyof` will never include `number`** > > Though TypeScript's `keyof` operator can yield a `number`, the concept of > numeric keys does not exist in JavaScript at runtime. This leads to confusing > and inconsistent behavior. In ArkType, `keyof` will always return a `string` > or `symbol` in accordance with the construction of a JavaScript object. > >
> Learn more about our motivation for diverging from TypeScript on this issue > > In JavaScript, you can use a number literal to define a key, but the constructed value has no way to represent a numeric key, so it is coerced to a string. > > ```ts > const numberLiteralObj = { > 4: true, > 5: true > } > > const stringLiteralObj = { > "4": true, > "5": true > } > > // numberLiteralObj and stringLiteralObj are indistinguishable at this point > Object.keys(numberLiteralObj) // ["4", "5"] > Object.keys(stringLiteralObj) // ["4", "5"] > ``` > > For a set-based type system to be correct, any two types representing the same set of underlying values must share a single representation. TypeScript's decision to have distinct numeric and string representations for the same underlying key has led to some if its most confusing inference pitfalls: > > ```ts > type Thing1 = { > [x: string]: unknown > } > > // Thing2 is apparently identical to Thing1 > type Thing2 = Record > > // and yet... > type Key1 = keyof Thing1 > > type Key2 = keyof Thing2 > ``` > > This sort of inconsistency is inevitable for a type system that has to reconcile multiple representations > for identical sets of underlying values. Therefore, numeric keys are one of a handful of cases where ArkType intentionally diverges from TypeScript. ArkType will never return a `number` from `keyof`. Keys will always be normalized to a `string` or `symbol`, the two distinct property types that can be uniquely attached to a JavaScript object. > >
### get Like an index access expression in TypeScript (e.g. `User["name"]`), the `get` operator extracts the Type of a value based on a specified key definition from an object: ```ts const snorfUsage = type.enumerated("eating plants", "looking adorable") const Manatee = type({ isFriendly: "true", snorf: { uses: snorfUsage.array() } }) const True = Manatee.get("isFriendly") // nested properties can be accessed directly by passing additional args const SnorfUses = Manatee.get("snorf", "uses") ``` > **Expressions like `get` and `omit` that extract a portion of an existing Type can be an antipattern!** > > Before using `get` to extract the type of a property you've defined, consider > whether you may be able to define the property value directly as a standalone > Type that can be easily referenced and composed as needed. > > Usually, composing Types from the bottom up is clearer and more efficient than trying to rip the part you need out of an existing Type. Though cases like this are quite straightforward, there are a number of more nuanced behaviors to consider when accessing an arbitrary key that could be a union, literal, or index signature on an object Type that could also be a union including optional keys or index signatures. If you're interested in a deeper dive into this (or anything else in ArkType), [our unit tests](https://github.com/arktypeio/arktype/blob/main/ark/type/__tests__/get.test.ts) are the closest thing we have to a comprehensive spec. Not your cup of tea? No worries- the inferred types and errors you'll see in editor will always be guiding you in the right direction 🧭 > **Support for TypeScript's index access syntax is planned!** > > Leave a comment on [the > issue](https://github.com/arktypeio/arktype/issues/831) letting us know if > you're interested in using- or even helping implement- type-level parsing for > string-embedded index access πŸ€“ ### structural transformations `merge`, `pick`, `omit`, `required`, `partial` and `map` each return a **new object built from the properties** of the `Type` they're applied to rather than referencing it. > **Only properties are carried over** > > Anything attached to the object itself is not part of that result- most importantly a root [narrow](https://arktype.io/docs/expressions#narrow) or [filter](https://arktype.io/docs/expressions#filter), but also [metadata](https://arktype.io/docs/configuration#metadata) like `.describe(...)` and constraints like `.atLeastLength(...)`. Constraints on individual properties are unaffected. > > ```ts > const User = type({ > name: "string", > email: "string" > }).narrow(user => user.email.includes("@")) > > // Type<{ name?: string; email?: string }>- the narrow is not a property > const PartialUser = User.partial() > > // no error, though User itself would have rejected this > const out = PartialUser({ email: "not an email" }) > ``` > > Reapply anything you still need to the result: > > ```ts > const PartialUser = type({ > name: "string", > email: "string" > }) > .partial() > .narrow(user => user.email?.includes("@") ?? true) > ``` > > A root [morph](https://arktype.io/docs/expressions#pipe) is the exception. Since it can't be reduced to a set of properties, transforming a `Type` like `type({ name: "string" }).pipe(user => user)` throws a `ParseError` rather than discarding it. ### pick / omit Extract or exclude specific properties from an object Type: ```ts const User = type({ name: "string", email: "string.email", "age?": "number" }) // Type<{ name: string; email: string }> const NameAndEmail = User.pick("name", "email") // Type<{ name: string; age?: number }> const WithoutEmail = User.omit("email") ``` These are also available as [generic keywords](https://arktype.io/docs/generics): `Pick(User, "name | email")`, `Omit(User, "email")`. Like ArkType's other [structural transformations](https://arktype.io/docs/objects#properties-structural), `pick` and `omit` return a new object built from the properties they extract, so a root narrow or metadata is not carried over. ### required / partial Make all named properties required or optional: ```ts const User = type({ name: "string", "email?": "string.email", "age?": "number" }) // Type<{ name: string; email: string; age: number }> const RequiredUser = User.required() const Config = type({ host: "string", port: "number" }) // Type<{ host?: string; port?: number }> const PartialConfig = Config.partial() ``` These are also available as [generic keywords](https://arktype.io/docs/generics): `Required(User)`, `Partial(Config)`. Like ArkType's other [structural transformations](https://arktype.io/docs/objects#properties-structural), `required` and `partial` return a new object built from the properties they extract, so a root narrow or metadata is not carried over. > **Arrays and tuples mirror TS's `Required` and `Partial`** > > Like the homomorphic mapped types they're named for, `required` and `partial` preserve an array or tuple base and apply to its elements: > > ```ts > // Type<[string?, number?]> > const PartialPair = type(["string", "number"]).partial() > > // Type<[string, number]> > const RequiredPair = type(["string", "number?"]).required() > ``` > > They only ever change whether an element must be **present**, never the values it allows. TS additionally unions variadic elements and index signature values with `undefined`, since it has no modifier to mark either optional. ArkType doesn't, so `type("string[]").partial()` is still `string[]`. > > A postfix element can't be optional- `[string?, ...number[], boolean]` is as unrepresentable in ArkType as it is in TS. `partial` therefore throws a `ParseError` on a tuple like `[string, ...number[], boolean]` rather than widening every element the way TS's `Partial` does. ### readonly Mark all properties as readonly (type-level only, no runtime effect): ```ts const Frozen = type({ id: "number", name: "string" }).readonly() // Type<{ readonly id: number; readonly name: string }> ``` ### map Transform the properties of an object Type using a mapping function. The mapper receives a prop entry with `key`, `value`, and `kind` (`"required"` or `"optional"`). Return a `{ key, value }` object (optionally with `kind`) to transform, or an empty array `[]` to remove the property. Like ArkType's other [structural transformations](https://arktype.io/docs/objects#properties-structural), `map` returns a new object built from the properties it extracts, so a root narrow or metadata is not carried over. ```ts const User = type({ name: "string", "age?": "number" }) // make all properties nullable by wrapping each value in a union with null const NullableUser = User.map(prop => { return { key: prop.key, value: prop.value.or("null") } }) // Type<{ name: string | null; age?: number | null }> ``` ### props The `props` getter returns an array of property descriptors for introspection: ```ts const User = type({ name: "string", "age?": "number", role: "'admin' | 'user' = 'user'" }) for (const prop of User.props) { console.log(prop.kind) // "required", "optional" console.log(prop.key) // "name", "role", "age" console.log(prop.value.expression) // "string", '"admin" | "user"', "number" console.log(prop.meta) // metadata object if ("default" in prop) console.log(prop.default) // "user" for role } ``` ## arrays **string** ```ts const Arrays = type({ key: "string[]" }) ``` **fluent** ```ts const Arrays = type({ key: type.string.array() }) ``` **tuple** ```ts const Arrays = type({ key: [{ name: "string" }, "[]"] }) ``` **args** ```ts const Arrays = type({ key: type({ name: "string" }, "[]") }) ``` ### lengths Constrain an array with an inclusive or exclusive min or max length. **string** ```ts const Bounded = type({ nonEmptyStringArray: "string[] > 0", atLeast3Integers: "number.integer[] >= 3", lessThan10Emails: "string.email[] < 10", atMost5Booleans: "boolean[] <= 5" }) ``` **fluent** ```ts const Bounded = type({ nonEmptyStringArray: type.string.array().moreThanLength(0), atLeast3Integers: type.keywords.number.integer.array().atLeastLength(3), lessThan10Emails: type.keywords.string.email.array().lessThanLength(10), atMost5Booleans: type.boolean.array().atMostLength(5) }) ``` Range expressions allow you to specify both a min and max length and use the same syntax for exclusivity. **string** ```ts const Range = type({ nonEmptyStringArrayAtMostLength10: "0 < string[] <= 10", twoToFiveIntegers: "2 <= number.integer[] < 6" }) ``` **fluent** ```ts const Range = type({ nonEmptyStringArrayAtMostLength10: type.string .array() .moreThanLength(0) .atMostLength(10), twoToFiveIntegers: type.keywords.number.integer .array() .atLeastLength(2) .lessThanLength(6) }) ``` ## 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 **string** ```ts const MyTuple = type([ "string", // Object definitions can be nested in tuples- and vice versa! { coordinates: ["number", "number"] } ]) ``` **fluent** ```ts const MyTuple = type([ type.string, // Object definitions can be nested in tuples- and vice versa! { coordinates: [type.number, type.number] } ]) ``` ### defaultable Defaultable elements are optional elements that will be assigned their specified default if not present in the tuple's input. A tuple may include zero or more defaultable elements following its prefix elements and preceding its non-defaultable optional elements. Like optional elements, defaultable elements are mutually exclusive with postfix elements. **string** ```ts const MyTuple = type(["string", "boolean = false", "number = 0"]) ``` **fluent** ```ts const MyTuple = type([ type.string, type.boolean.default(false), type.number.default(0) ]) ``` **tuple** ```ts const MyTuple = type([ "string", [ { name: "string" }, "=", () => ({ name: "Anon Eemuss" }) ] ]) ``` ### optional Optional elements are tuple elements that may or may not be present in the input that do not have a default value. A tuple may include zero or more optional elements following its prefix and defaultable elements and preceding either a variadic element or the end of the tuple. Like in TypeScript, optional elements are mutually exclusive with postfix elements. **string** ```ts const MyTuple = type(["string", "bigint = 999n", "boolean?", "number?"]) ``` **fluent** ```ts const MyTuple = type([ type.string, type.bigint.default(999n), type.boolean.optional(), type.number.optional() ]) ``` **tuple** ```ts const MyTuple = type([ "string", [ { 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. **string** ```ts // allows a string followed by zero or more numbers const MyTuple = type(["string", "...", "number[]"]) ``` **fluent** ```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. **string** ```ts // allows zero or more numbers followed by a boolean, then a string const MyTuple = type(["...", "number[]", "boolean", "string"]) ``` **fluent** ```ts // allows zero or more numbers followed by a boolean, then a string const MyTuple = type(["...", type.number.array(), type.boolean, type.string]) ``` ## dates ### 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 Constrain a Date with an inclusive or exclusive min or max. Bounds can be expressed as either a [number](https://arktype.io/docs/primitives#number-literals) representing its corresponding Unix epoch value or a [Date literal](https://arktype.io/docs/objects#dates-literals). **string** ```ts const Bounded = type({ dateInThePast: `Date < ${Date.now()}`, dateAfter2000: "Date > d'2000-01-01'", dateAtOrAfter1970: "Date >= 0" }) ``` **fluent** ```ts const Bounded = type({ dateInThePast: type.Date.earlierThan(Date.now()), dateAfter2000: type.Date.laterThan("2000-01-01"), dateAtOrAfter1970: type.Date.atOrAfter(0) }) ``` Range expressions allow you to specify both a min and max and use the same syntax for exclusivity. **string** ```ts const tenYearsAgo = new Date() .setFullYear(new Date().getFullYear() - 10) .valueOf() const Bounded = type({ dateInTheLast10Years: `${tenYearsAgo} <= Date < ${Date.now()}` }) ``` **fluent** ```ts const tenYearsAgo = new Date() .setFullYear(new Date().getFullYear() - 10) .valueOf() const Bounded = type({ dateInTheLast10Years: type.Date.atOrAfter(tenYearsAgo).earlierThan(Date.now()) }) ``` ## instanceof Most built-in 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. > **Array is the only constructor that works cross-realm** > > Constructors are compared by identity, so a value created in another realm (e.g. an `