Objects

properties

Objects definitions can include any combination of required, optional, defaultable named properties and index signatures.

required

const  = Symbol()

const  = ({
	requiredKey: "string",
	// Nested definitions don't require additional `type` calls!
	[]: {
		nested: "unknown"
	}
})

optional

const  = Symbol()

const  = ({
	"optionalKey?": "number[]",
	[]: "string?"
})

Optional properties cannot be present with the value undefined

In TypeScript, there is a setting called 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. If you're interested in a builtin configuration option for this setting, we'd love feedback or contributions on this issue.

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

// valid data
const  = myObj({})

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

defaultable

const  = ({
	defaultableKey: "boolean = false"
})

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<number>, "?"] or [Type<number>, "=", 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:

const  = ("string | undefined").pipe(v => v ?? "")

index

const  = ({
	// 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!), ArkType does support rejection or deletion of undeclared keys. This behavior can be defined for individual objects using the syntax below or via configuration if you want to change the default across all objects.

// fail if any key other than "onlyAllowedKey" is present
const  = ({
	"+": "reject",
	onlyAllowedKey: "string"
})

// delete all non-symbolic keys other than "onlyPreservedStringKey"
const  = ({
	"+": "delete",
	"[symbol]": "unknown",
	onlyPreservedStringKey: "string"
})

// allow and preserve undeclared keys (the default behavior)
const  = ({
	// only specify "ignore" if you explicitly configured the default elsewhere
	"+": "ignore",
	nonexclusiveKey: "number"
})

spread

The spread operator is great for merging sets of properties. When applied to two distinct (i.e. non-overlapping) sets of properties, it is equivalent to 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.

A base object definition can be spread if "..." is the first key specified in an object literal. Subsequent properties will be merged into those from the base object, just like the ... operator in JS.

const  = ({ isAdmin: "false", name: "string" })

// hover to see the newly merged object
const  = ({
	"...": ,
	// in an intersection, non-overlapping values at isAdmin would result in a ParseError
	isAdmin: "true",
	permissions: "string[]"
})

The spread operator is semantically equivalent to the generic Merge keyword, which can be instantiated via a dedicated method on Type in addition to the standard keyword syntax.

const  = type.module({
	base: {
		"foo?": "0",
		"bar?": "0"
	},
	merged: {
		bar: "1",
		"baz?": "1"
	},
	result: "Merge<base, merged>"
})

// hover to see the inferred result
type  = typeof .result.infer

keyof

Like in TypeScript, the keyof operator extracts the keys of an object as a union:

const  = ({
	originallyPurchased: "string.date",
	remainingWheels: "number"
})

const  = .keyof()

type  = typeof .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:

const  = ({
	"[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  = .keyof()

// key is identical to the base `string` Type
console.log(.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.

const  = {
	4: true,
	5: true
}

const  = {
	"4": true,
	"5": true
}

// numberLiteralObj and stringLiteralObj are indistinguishable at this point
Object.keys() // ["4", "5"]
Object.keys() // ["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:

type  = {
	[x: string]: unknown
}

// Thing2 is apparently identical to Thing1
type  = <string, unknown>

// and yet...
type 
type Key1 = string | number
Key1
= keyof
type
type Key2 = string
Key2
= keyof

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:

const  = type.enumerated("eating plants", "looking adorable")

const  = ({
	isFriendly: "true",
	snorf: {
		uses: .array()
	}
})

const  = .get("isFriendly")

// nested properties can be accessed directly by passing additional args
const  = .get("snorf", "uses")

Expressions like `get` and `omit` that extract a portion of an exising 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 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 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 letting us know if you're interested in using- or even helping implement- type-level parsing for string-embedded index access 🤓

arrays

const  = ({
	key: "string[]"
})

lengths

Constrain an array with an inclusive or exclusive min or max length.

const  = ({
	nonEmptyStringArray: "string[] > 0",
	atLeast3Integers: "number.integer[] >= 3",
	lessThan10Emails: "string.email[] < 10",
	atMost5Booleans: "boolean[] <= 5"
})

Range expressions allow you to specify both a min and max length and use the same syntax for exclusivity.

const  = ({
	nonEmptyStringArrayAtMostLength10: "0 < string[] <= 10",
	twoToFiveIntegers: "2 <= number.integer[] < 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

const  = ([
	"string",
	// Object definitions can be nested in tuples- and vice versa!
	{
		coordinates: ["number", "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.

const  = (["string", "boolean = false", "number = 0"])

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.

const  = (["string", "bigint = 999n", "boolean?", "number?"])

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.

// allows a string followed by zero or more numbers
const  = (["string", "...", "number[]"])

postfix

Postfix elements are required elements following a variadic element.

They are mutually exclusive with optional elements.

// allows zero or more numbers followed by a boolean, then a string
const  = (["...", "number[]", "boolean", "string"])

dates

literals

Date literals represent a Date instance with an exact value.

They're primarily useful in ranges.

const  = ({
	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 representing its corresponding Unix epoch value or a Date literal.

const  = ({
	dateInThePast: `Date < ${Date.now()}`,
	dateAfter2000: "Date > d'2000-01-01'",
	dateAtOrAfter1970: "Date >= 0"
})

Range expressions allow you to specify both a min and max and use the same syntax for exclusivity.

const  = new Date()
	.setFullYear(new Date().getFullYear() - 10)
	.valueOf()

const  = ({
	dateInTheLast10Years: `${} <= Date < ${Date.now()}`
})

instanceof

Most builtin 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.

class MyClass {}

const  = type.instanceOf(MyClass)

keywords

A list of instanceof keywords can be found here alongside the base and subtype keywords for Array and FormData.

On this page