Skip to content

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.

In other words, they just work.

Define

Let’s create a new contact Type that enforces our example constraints.

// hover to see the type-level representation
const 
const contact: Type<{
    email: string.email;
    score: number.is<LessThan<100> & DivisibleBy<1>>;
}>
contact
=
const type: TypeParser
<{
    readonly email: "string.email";
    readonly score: "number.integer < 100";
}, Type<{
    email: string.email;
    score: number.is<LessThan<100> & DivisibleBy<1>>;
}, {}>>(def: validateObjectLiteral<...>) => Type<...> (+2 overloads)
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
type Contact = {
    email: string;
    score: number;
}
Contact
= typeof
const contact: Type<{
    email: string.email;
    score: number.is<LessThan<100> & DivisibleBy<1>>;
}>
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.

// a non-empty list of Contact
const const contacts: Type<constrain<Contact[], AtLeastLength<1>>>contacts = const contact: Type<Contact>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.

const const palindromicEmail: Type<string.is<Narrowed & Branded<"email">>>palindromicEmail = 
const type: TypeParser
<"string.email", Type<string.email, {}>>(def: "string.email") => Type<string.email, {}> (+2 overloads)
type
("string.email").narrow((address, ctx) => {
if (address === [...address].reverse().join("")) { // congratulations! your email is somehow a palindrome return true } // add a customizable error and return false return ctx.mustBe("a palindrome") }) const
const palindromicContact: Type<{
    email: string.is<Narrowed & Branded<"email">>;
    score: number.is<LessThan<100> & DivisibleBy<1>>;
}>
palindromicContact
=
const type: TypeParser
<{
    readonly email: Type<string.is<Narrowed & Branded<"email">>, {}>;
    readonly score: "number.integer < 100";
}, Type<{
    email: string.is<Narrowed & Branded<...>>;
    score: number.is<...>;
}, {}>>(def: validateObjectLiteral<...>) => Type<...> (+2 overloads)
type
({
email: const palindromicEmail: Type<string.is<Narrowed & Branded<"email">>>palindromicEmail, score: "number.integer < 100" })

We can invoke palindromicContact anywhere to get validated data or a list of errors with a user-friendly summary.

const 
const out: ArkErrors | {
    email: string;
    score: number;
}
out
= palindromicContact({
email: "david@arktype.io", score: 133.7 }) if (
const out: ArkErrors | {
    email: string;
    score: number;
}
out
instanceof type.errors) {
// hover summary to see validation errors console.error(const out: RuntimeErrorsout.RuntimeErrors.summary: string
email must be a palindrome (was "david@arktype.io") score (133.7) must be... • an integer • less than 100
summary
)
} else { console.log(
const out: {
    email: string;
    score: number;
}
out
.email)
}

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.