📈 Announcing ArkType 2.1 📈

Configuration

A great out-of-the-box experience is a core goal of ArkType, including safe defaults and helpful messages for complex errors.

However, it's equally important that when you need different behavior, you can easily configure it with the right granularity.

LevelApplies ToExample
defaultbuiltin defaults for all Types
globalall Types parsed after the config is applied
config.ts
import { configure } from "arktype/config"
// use the "arktype/config" entrypoint
configure({ numberAllowsNaN: true })
app.ts
import "./config.ts"
// import your config file before arktype
import { type } from "arktype"

type.number.allows(Number.NaN) // true
scopeall Types parsed in the configured Scope
const  = (
	{ user: { age: "number < 100" } },
	{
		max: {
			actual: () => "unacceptably large"
		}
	}
)
const  = .export()
// ArkErrors: age must be less than 100 (was unacceptably large)
.user({ name: "Alice", age: 101 })
const  = .type({
	age: "number <= 100"
})
// ArkErrors: age must be at most 100 (was unacceptably large)
parsedAfter({ age: 101 })
typeall Types shallowly referenced by the configured Type
// avoid logging "was xxx" for password
const  = ("string >= 8").configure({ actual: () => "" })
const  = ({
	email: "string.email",
	password
})
// ArkErrors: password must be at least length 8
const  = user({
	email: "david@arktype.io",
	password: "ez123"
})

Some options only apply at specific levels, as reflected in the corresponding input types.

Use the `"arktype/config"` entrypoint in a separate file for global config!

If you need your config to apply to builtin keywords (important for options like jitless, numberAllowsNaN, dateAllowsInvalid), you should import and configure from "arktype/config" before importing anything from "arktype".

Otherwise, keywords will have already been parsed by the time your config applies!

Errors

To allow custom errors to be integrated seemlessly with builtin logic for composite errors (i.e. union and intersection), ArkType supports a set of composable options:

optionaldescriptionexample
description

✅ a summary of the constraint that could complete the phrase "must be ___"

🥇 reused by other metadata and should be your first go-to for customizing a message

const  = type.string.atLeastLength(8).describe("a valid password")
// ArkErrors: must be a valid password
const  = password("ez123")
expected

✅ a function accepting the error context and returning a string of the format "must be ___"

✅ specific to errors and takes precedence over description in those cases

const  = type.string.atLeastLength(8).configure({
	expected: ctx =>
		ctx.code === "minLength" ? `${ctx.rule} characters or better` : "way better"
})
// ArkErrors: must be 8 characters or better (was 5)
const  = password("ez123").toString()
// ArkErrors: must be way better (was a number)
const  = password(12345678).toString()
actual

✅ a function accepting the data that caused the error and returning a string of the format "(was ___)"

✅ if an empty string is returned, the actual portion of the message will be omitted

const  = ("string >= 8").configure({ actual: () => "" })
// ArkErrors: password must be at least length 8
const  = password("ez123")
problem

✅ a function accepting the results of expected and actual in addition to other context and returning a complete description of the problem like "must be a string (was a number)"

❌ may not apply to composite errors like unions

const  = ("string >= 8").configure({
	problem: ctx => `${ctx.actual} isn't ${ctx.expected}`
})
// ArkErrors: 5 isn't at least length 8
const  = password("ez123")
// ArkErrors: a number isn't a string
const  = password(12345678)
message

✅ a function accepting the result of problem in addition to other context and returning a complete description of the problem including the path at which it occurred

❌ may not apply to composite errors like unions

const  = ({
	password: "string >= 8"
}).configure({
	message: ctx =>
		`${ctx.propString || "(root)"}: ${ctx.actual} isn't ${ctx.expected}`
})
// ArkErrors: (root): a string isn't an object
const  = user("ez123")
// `.configure` only applies shallowly, so the nested error isn't changed!
// ArkErrors: password must be at least length 8 (was 5)
const  = user({ password: "ez123" })

By Code

Errors can also be configured by their associated code property at a scope or global level.

For example:

const  = type.module(
	{ isEven: "number%2" },
	{
		divisor: {
			// the available `ctx` types will include data specific to your errors
			expected: ctx => `% ${ctx.rule} !== 0`,
			problem: ctx => `${ctx.actual} ${ctx.expected}`
		}
	}
)
// ArkErrors: 3 % 2 !== 0
.isEven(3)

ArkErrors

For use cases like i18n that fall outside the scope of this composable message config, the ArkErrors array returned on validation failure contains ArkError instances that can be discriminated via calls like .hasCode("divisor") and contain contextual data specific to that error type as well as getters for each composable error part.

These ArkError instances can be arbitrarily transformed and composed with an internationalization library. This is still a topic we're working on investigating and documenting, so please reach out with any questions or feedback!

Keywords

Builtin keywords like string.email can be globally configured.

This can be very helpful for customizing error messages without needing to create your own aliases or wrappers.

config.ts
import { configure } from "arktype/config"

configure({
	keywords: {
		string: "shorthand description",
		"string.email": {
			actual: () => "definitely fake"
		}
	}
})
app.ts
import "./config.ts"
// import your config file before arktype
import { type } from "arktype"

const  = type({
	name: "string",
	email: "string.email"
})

const  = user({
	// ArkErrors: name must be shorthand description (was a number)
	name: 5,
	// ArkErrors: email must be an email address (was definitely fake)
	email: "449 Canal St"
})

The options you can provide here are identical to those used to configure a Type directly, and can also be extended at a type-level to include custom metadata.

Clone

By default, before a morph is applied, ArkType will deeply clone the original input value with a builtin deepClone function that tries to make reasonable assumptions about preserving prototypes etc. The implementation of deepClone can be found here.

You can provide an alternate clone implementation to the clone config option.

config.ts
import { configure } from "arktype/config"

configure({ clone: structuredClone })
app.ts
import "./config.ts"
// import your config file before arktype
import { type } from "arktype"

// will now create a new object using structuredClone
const  = type({
	age: "string.numeric.parse"
})

To mutate the input object directly, you can set the clone config option to false.

config.ts
import { configure } from "arktype/config"

configure({ clone: false })
app.ts
import "./config.ts"
// import your config file before arktype
import { type } from "arktype"

const  = type({
	age: "string.numeric.parse"
})

const  = {
	age: "42"
}

const  = userForm()

// the original object's age key is now a number
console.log(.age)

onUndeclaredKey

Like TypeScript, ArkType defaults to ignoring undeclared keys during validation. However, it also supports two additional behaviors:

  • "ignore" (default): Allow undeclared keys on input, preserve them on output
  • "delete": Allow undeclared keys on input, delete them before returning output
  • "reject": Reject input with undeclared keys

These behaviors can be associated with individual Types via the builtin "+" syntax (see those docs for more on how they work). You can also change the default globally:

config.ts
import { configure } from "arktype/config"

configure({ onUndeclaredKey: "delete" })
app.ts
import "./config.ts"
// import your config file before arktype
import { type } from "arktype"

const  = type({
	name: "string"
})

// out is now { name: "Alice" }
const  = userForm({
	name: "Alice",
	age: "42"
})

exactOptionalPropertyTypes

By default, ArkType validates optional keys as if TypeScript's exactOptionalPropertyTypes is set to true.

See an example
const  = ({
	"key?": "number"
})

// valid data
const  = myObj({})

// Error: key must be a number (was undefined)
const  = myObj({ key: undefined })

This approach allows the most granular control over optionality, as | undefined can be added to properties that should accept it.

However, if you have not enabled TypeScript's exactOptionalPropertyTypes setting, you may globally configure ArkType's exactOptionalPropertyTypes to false to match TypeScript's behavior. If you do this, we'd recommend making a plan to enable exactOptionalPropertyTypes in the future.

config.ts
import { configure } from "arktype/config"

// since the default in ArkType is `true`, this will only have an effect if set to `false`
configure({ exactOptionalPropertyTypes: false })
app.ts
import "./config.ts"
// import your config file before arktype
import { type } from "arktype"

const  = type({
	"key?": "number"
})

// valid data
const  = myObj({})

// now also valid data (would be an error by default)
const  = myObj({ key: undefined })

exactOptionalPropertyTypes does not yet affect default values!

const  = ({
	key: "number = 5"
})

// { key: 5 }
const  = myObj({})

// { key: undefined }
const  = myObj({ key: undefined })

Support for this is tracked as part of this broader configurable defaultability issue.

jitless

By default, when a Type is instantiated, ArkType will precompile optimized validation logic that will run when the type is invoked. This behavior is disabled by default in environments that don't support new Function, e.g. Cloudflare Workers.

If you'd like to opt out of it for another reason, you can set the jitless config option to true.

config.ts
import { configure } from "arktype/config"

configure({ jitless: true })
app.ts
import "./config.ts"
// import your config file before arktype
import { type } from "arktype"

// will not be precompiled
const  = type({
	foo: "string"
})

onFail

In some domains, you may always want to throw on failed validation or transform the result in some other way.

By specifying onFail in your global config, you can control what happens when you invoke a Type on invalid data:

config.ts
import { configure } from "arktype/config"

const  = configure({
	onFail: errors => errors.throw()
})

// be sure to specify both the runtime and static configs

declare global {
	interface ArkEnv {
		onFail: typeof .onFail
	}
}
app.ts
import "./config.ts"
// import your config file before arktype
import { type } from "arktype"

// data is inferred as string- no need to discriminate!
const  = type.string("foo")

// now thrown instead of returned
// ArkErrors: must be a string (was number)
const  = type.string(5)

metadata

Additional arbitrary metadata can also be associated with a Type.

It can even be made type-safe via an interface extension ArkType exposes for this purpose:

// add this anywhere in your project
declare global {
	interface ArkEnv {
		meta(): {
			// meta properties should always be optional
			secretIngredient?: string
		}
	}
}

// now types you define can specify and access your metadata
const  = ({
	broth: "'miso' | 'vegetable'",
	ingredients: "string[]"
}).configure({ secretIngredient: "nothing!" })

prototypes

When you .infer your Types, ArkType traverses them and extracts special values like morphs, e.g. (In: string) => Out<number>.

Though generally this is able to preserve the original type, it is inefficient and can accidentally expand certain object types.

You can use the type-level prototypes config to tell ArkType to treat those types as external:

declare global {
	interface ArkEnv {
		prototypes(): MySpecialClass
	}
}

class MySpecialClass {}

const 
const t: Type<MySpecialClass, {}>
t
= type.instanceOf(MySpecialClass)