Expressions

Intersection

Like its TypeScript counterpart, an intersection combines two existing Types to create a new Type that enforces the constraints of both.

const  = ({
	// an email address with the domain arktype.io
	intersected: "string.email & /@arktype\\.io$/"
})

Union

All unions are automatically discriminated to optimize check time and error message clarity.

const  = ({
	key: "string | number"
})

A union that could apply different morphs to the same data throws a ParseError!

// operands overlap, but neither transforms data
const  = ("number > 0").or("number < 10")
// operand transforms data, but there's no overlap between the inputs
const  = ("string.numeric.parse").or({ box: "string" })
// operands overlap and transform data, but in the same way
const  = ("string > 5", "=>", Number.parseFloat).or([
	"0 < string < 10",
	"=>",
	Number.parseFloat
])
// ParseError: An unordered union of a type including a morph and a type with overlapping input is indeterminate
const  = ({ box: "string.numeric.parse" }).or({ box: "string" })
const  = ({ a: "string.numeric.parse" }).or({ b: "string.numeric.parse" })
Learn the basic set theory behind this restriction

If you're relatively new to set-based types, that error might be daunting, but if you take a second to think through the example, it becomes clear why this isn't allowed. The logic of bad is essentially:

  • If the input is an object where box is a string, parse and return it as a number
  • If the input is an object where box is a string, return it as a string

There is no way to deterministically return an output for this type without sacrificing the commutativity of the union operator.

sameError may look more innocuous, but has the same problem for an input like { a: "1", b: "2" }.

  • Left branch would only parse a, resulting in { a: 1, b: "2" }
  • Right branch would only parse b, resulting in { a: "1", b: 2 }

Brand

Add a type-only symbol to an existing type so that the only values that satisfy is are those that have been directly validated.

// hover to see ArkType's representation of a branded type
const  = ({
	brandedString: "string#id"
})

Narrow

Narrow expressions allow you to add custom validation logic and error messages. You can read more about them in their intro section.

const  = ({
	password: "string",
	confirmPassword: "string"
}).narrow((data, ctx) => {
	if (data.password === data.confirmPassword) {
		return true
	}
	return ctx.reject({
		expected: "identical to password",
		// don't display the password in the error message!
		actual: "",
		path: ["confirmPassword"]
	})
})

// ArkErrors: confirmPassword must be identical to password
const  = form({
	password: "arktype",
	confirmPassword: "artkype"
})

If the return type of a narrow is a type predicate, that will be reflected in the inferred Type.

// hover to see how the predicate is propagated to the outer `Type`
const  = ("string").narrow(
	(data, ctx): data is `ark${string}` =>
		data.startsWith("ark") ?? ctx.reject("a string starting with 'ark'")
)

Morph

Morphs allow you to transform your data after it is validated. You can read more about them in their intro section.

// hover to see how morphs are represented at a type-level
const  = ("string").pipe(str => str.trimStart())

Unit

While embedded literal syntax is usually ideal for defining exact primitive values, === and type.unit can be helpful for referencing a non-serialiazable value like a symbol from your type.

const  = Symbol()

const  = type.unit()

Enumerated

type.enumerated defines a type based on a list of allowed values. It is semantically equivalent to type.unit if provided a single value.

const  = Symbol()

const  = type.enumerated(1337, true, )

Meta

Metadata allows you to associate arbitrary metadata with your types.

Some metadata is consumed directly by ArkType, for example description is referenced by default when building an error message.

Other properties are introspectable, but aren't used by default internally.

// this validator's error message will now start with "must be a special string"
const  = ("string").configure({
	description: "a special string"
})

// sugar for adding description metadata
const  = ("number").describe("a special number")

Cast

Sometimes, you may want to directly specify how a Type should be inferred without affecting the runtime behavior. In these cases, you can use a cast expression.

// allow any string, but suggest "foo" and "bar"
type  = "foo" | "bar" | (string & {})

const  = ({
	autocompletedString: "string" as type.<>
})

Parenthetical

By default, ArkType's operators follow the same precedence as TypeScript's. Also like in TypeScript, this can be overridden by wrapping an expression in parentheses.

// hover to see the distinction!
const  = ({
	stringOrArrayOfNumbers: "string | number[]",
	arrayOfStringsOrNumbers: "(string | number)[]"
})

this

this is a special keyword that can be used to create a recursive type referencing the root of the current definition.

const  = ({ label: "string", "box?": "this" })
const  = .assert(await ())

// hover me
const  = .box?.box?.label

Unlike its TypeScript counterpart, ArkType's this is not limited to interfaces. It can also be used from within a tuple expression.

// boxes now expects an array of our gift object
const  = ({ label: "string", boxes: "this" }, "[]")

Referencing this from within a scope will result in a ParseError. For similar behavior within a scoped definition, just reference the alias by name:

const  = ({
	disappointingGift: {
		label: "string",
		// Resolves correctly to the root of the current type
		"box?": "disappointingGift"
	}
}).export()

On this page