Expressions
Intersection
Like its TypeScript counterpart, an intersection combines two existing Type
s to create a new Type
that enforces the constraints of both.
const const myObject: Type<{
intersected: string;
}>
myObject = const type: TypeParser
<{
readonly intersected: "string.email & /@arktype\\.io$/";
}, Type<{
intersected: string;
}, {}>>(def: validateObjectLiteral<{
readonly intersected: "string.email & /@arktype\\.io$/";
}, {}, bindThis<{
readonly intersected: "string.email & /@arktype\\.io$/";
}>>) => Type<...> (+2 overloads)
type({
// an email address with the domain arktype.io
intersected: "string.email & /@arktype\\.io$/"
})
const const fooObject: Type<{
foo: string;
}>
fooObject = const type: TypeParser
<{
readonly foo: "string";
}, Type<{
foo: string;
}, {}>>(def: validateObjectLiteral<{
readonly foo: "string";
}, {}, bindThis<{
readonly foo: "string";
}>>) => Type<...> (+2 overloads)
type({
foo: "string"
})
// an object requiring both foo and bar
const const foobarObject: Type<{
foo: string;
bar: number;
}>
foobarObject = const fooObject: Type<{
foo: string;
}>
fooObject.and({
bar: "number"
})
// an object requiring both foo and bar
const const foobarObject: Type<{
foo: string;
bar: number;
}>
foobarObject = const type: TypeParser
<readonly [{
readonly foo: "string";
}, "&", {
readonly bar: "number";
}], Type<{
foo: string;
bar: number;
}, {}>>(def: readonly [validateObjectLiteral<{
readonly foo: "string";
}, {}, bindThis<readonly [{
readonly foo: "string";
}, "&", {
readonly bar: "number";
}]>>, "&", validateObjectLiteral<...>]) => Type<...> (+2 overloads)
type([
{
foo: "string"
},
"&",
{
bar: "number"
}
])
// an object requiring both foo and bar
const const foobarObject: Type<{
foo: string;
bar: number;
}>
foobarObject = const type: TypeParser
<{
readonly foo: "string";
}, "&", readonly [{
readonly bar: "number";
}], Type<{
foo: string;
bar: number;
}, {}>>(_0: validateObjectLiteral<{
readonly foo: "string";
}, {}, bindThis<{
readonly foo: "string";
}>>, _1: "&", _2_0: validateObjectLiteral<...>) => Type<...> (+2 overloads)
type(
{
foo: "string"
},
"&",
{
bar: "number"
}
)
Union
All unions are automatically discriminated to optimize check time and error message clarity.
const const unions: Type<{
key: string | number;
}>
unions = const type: TypeParser
<{
readonly key: "string | number";
}, Type<{
key: string | number;
}, {}>>(def: validateObjectLiteral<{
readonly key: "string | number";
}, {}, bindThis<{
readonly key: "string | number";
}>>) => Type<...> (+2 overloads)
type({
key: "string | number"
})
const const unions: Type<{
key: string | number;
}>
unions = const type: TypeParser
<{
readonly key: Type<string | number, {}>;
}, Type<{
key: string | number;
}, {}>>(def: validateObjectLiteral<{
readonly key: Type<string | number, {}>;
}, {}, bindThis<{
readonly key: Type<string | number, {}>;
}>>) => Type<...> (+2 overloads)
type({
key: type.string.or(type.number)
})
const const unions: Type<{
key: string | {
name: string;
};
}>
unions = const type: TypeParser
<{
readonly key: readonly ["string", "|", {
readonly name: "string";
}];
}, Type<{
key: string | {
name: string;
};
}, {}>>(def: validateObjectLiteral<{
readonly key: readonly ["string", "|", {
readonly name: "string";
}];
}, {}, bindThis<...>>) => Type<...> (+2 overloads)
type({
key: ["string", "|", { name: "string" }]
})
const const unions: Type<{
key: string | {
name: string;
};
}>
unions = const type: TypeParser
<{
readonly key: Type<string | {
name: string;
}, {}>;
}, Type<{
key: string | {
name: string;
};
}, {}>>(def: validateObjectLiteral<{
readonly key: Type<string | {
name: string;
}, {}>;
}, {}, bindThis<...>>) => Type<...> (+2 overloads)
type({
key: const type: TypeParser
<"string", "|", readonly [{
readonly name: "string";
}], Type<string | {
name: string;
}, {}>>(_0: "string", _1: "|", _2_0: validateObjectLiteral<{
readonly name: "string";
}, {}, bindThis<{
readonly name: "string";
}>>) => Type<...> (+2 overloads)
type("string", "|", { name: "string" })
})
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 const myObject: Type<{
brandedString: Brand<string, "id">;
}>
myObject = const type: TypeParser
<{
readonly brandedString: "string#id";
}, Type<{
brandedString: Brand<string, "id">;
}, {}>>(def: validateObjectLiteral<{
readonly brandedString: "string#id";
}, {}, bindThis<{
readonly brandedString: "string#id";
}>>) => Type<...> (+2 overloads)
type({
brandedString: "string#id"
})
// hover to see ArkType's representation of a branded type
const const myObject: Type<{
brandedString: Brand<string, "id">;
}>
myObject = const type: TypeParser
<{
readonly brandedString: Type<Brand<string, "id">, {}>;
}, Type<{
brandedString: Brand<string, "id">;
}, {}>>(def: validateObjectLiteral<{
readonly brandedString: Type<Brand<string, "id">, {}>;
}, {}, bindThis<...>>) => Type<...> (+2 overloads)
type({
brandedString: type.string.brand("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 const form: Type<{
password: string;
confirmPassword: string;
}>
form = const type: TypeParser
<{
readonly password: "string";
readonly confirmPassword: "string";
}, Type<{
password: string;
confirmPassword: string;
}, {}>>(def: validateObjectLiteral<{
readonly password: "string";
readonly confirmPassword: "string";
}, {}, bindThis<...>>) => Type<...> (+2 overloads)
type({
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 const out: {
password: string;
confirmPassword: string;
} | ArkErrors
out = form({
password: "arktype",
confirmPassword: "artkype"
})
const const form: Type<{
password: string;
confirmPassword: string;
}>
form = const type: TypeParser
<readonly [{
readonly password: "string";
readonly confirmPassword: "string";
}, ":", (data: {
password: string;
confirmPassword: string;
}, ctx: TraversalContext) => boolean], Type<...>>(def: readonly [...]) => Type<...> (+2 overloads)
type([
{
password: "string",
confirmPassword: "string"
},
":",
(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 const out: {
password: string;
confirmPassword: string;
} | ArkErrors
out = form({
password: "arktype",
confirmPassword: "artkype"
})
const const form: Type<{
password: string;
confirmPassword: string;
}>
form = const type: TypeParser
<{
readonly password: "string";
readonly confirmPassword: "string";
}, ":", readonly [(data: {
password: string;
confirmPassword: string;
}, ctx: TraversalContext) => boolean], Type<...>>(_0: validateObjectLiteral<...>, _1: ":", _2_0: Predicate<...>) => Type<...> (+2 overloads)
type(
{
password: "string",
confirmPassword: "string"
},
":",
(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 const out: ArkErrors | {
password: string;
confirmPassword: string;
}
out = 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 const arkString: Type<`ark${string}`>
arkString = const type: TypeParser
<"string", Type<string, {}>>(def: "string") => Type<string, {}> (+2 overloads)
type("string").narrow(
(data, ctx): data is `ark${string}` =>
data.startsWith("ark") ?? ctx.reject("a string starting with 'ark'")
)
// hover to see how the predicate is propagated to the outer `Type`
const const arkString: Type<`ark${string}`>
arkString = const type: TypeParser
<readonly ["string", ":", (data: string, ctx: TraversalContext) => data is `ark${string}`], Type<`ark${string}`, {}>>(def: readonly ["string", ":", Predicate<string>]) => Type<...> (+2 overloads)
type([
"string",
":",
(data, ctx): data is `ark${string}` =>
data.startsWith("ark") ?? ctx.reject("a string starting with 'ark'")
])
// hover to see how the predicate is propagated to the outer `Type`
const const arkString: Type<`ark${string}`>
arkString = const type: TypeParser
<"string", ":", readonly [(data: string, ctx: TraversalContext) => data is `ark${string}`], Type<`ark${string}`, {}>>(_0: "string", _1: ":", _2_0: Predicate<string>) => Type<...> (+2 overloads)
type(
"string",
":",
(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 const trimStringStart: Type<(In: string) => Out<string>>
trimStringStart = const type: TypeParser
<"string", Type<string, {}>>(def: "string") => Type<string, {}> (+2 overloads)
type("string").pipe(str => str.trimStart())
// hover to see how morphs are represented at a type-level
const const trimStringStart: Type<(In: string) => Out<string>>
trimStringStart = const type: TypeParser
<readonly ["string", "=>", (str: string) => string], Type<(In: string) => Out<string>, {}>>(def: readonly ["string", "=>", Morph<string>]) => Type<(In: string) => Out<string>, {}> (+2 overloads)
type(["string", "=>", str => str.trimStart()])
// hover to see how morphs are represented at a type-level
const const trimStringStart: Type<(In: string) => Out<string>>
trimStringStart = const type: TypeParser
<"string", "=>", readonly [(str: string) => string], Type<(In: string) => Out<string>, {}>>(_0: "string", _1: "=>", _2_0: Morph<string, unknown>) => Type<(In: string) => Out<string>, {}> (+2 overloads)
type("string", "=>", 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 const mySymbol: typeof mySymbol
mySymbol = Symbol()
const const exactValue: Type<typeof mySymbol>
exactValue = type.unit(const mySymbol: typeof mySymbol
mySymbol)
const const mySymbol: typeof mySymbol
mySymbol = Symbol()
const const exactValue: Type<typeof mySymbol>
exactValue = const type: TypeParser
<readonly ["===", typeof mySymbol], Type<typeof mySymbol, {}>>(def: readonly ["===", ...unknown[]]) => Type<typeof mySymbol, {}> (+2 overloads)
type(["===", const mySymbol: typeof mySymbol
mySymbol])
const const mySymbol: typeof mySymbol
mySymbol = Symbol()
const const exactValue: Type<typeof mySymbol>
exactValue = const type: TypeParser
<"===", typeof mySymbol, readonly [], Type<typeof mySymbol, {}>>(_0: "===", _1: typeof mySymbol) => Type<typeof mySymbol, {}> (+2 overloads)
type("===", const mySymbol: typeof mySymbol
mySymbol)
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 const mySymbol: typeof mySymbol
mySymbol = Symbol()
const const exactValueFromSet: Type<true | typeof mySymbol | 1337>
exactValueFromSet = type.enumerated(1337, true, const mySymbol: typeof mySymbol
mySymbol)
const const mySymbol: typeof mySymbol
mySymbol = Symbol()
const const exactValueFromSet: Type<true | typeof mySymbol | 1337>
exactValueFromSet = const type: TypeParser
<readonly ["===", 1337, true, typeof mySymbol], Type<true | typeof mySymbol | 1337, {}>>(def: readonly ["===", ...unknown[]]) => Type<true | typeof mySymbol | 1337, {}> (+2 overloads)
type(["===", 1337, true, const mySymbol: typeof mySymbol
mySymbol])
const const mySymbol: typeof mySymbol
mySymbol = Symbol()
const const exactValueFromSet: Type<true | typeof mySymbol | 1337>
exactValueFromSet = const type: TypeParser
<"===", 1337, readonly [true, typeof mySymbol], Type<true | typeof mySymbol | 1337, {}>>(_0: "===", _1: 1337, _2_0: true, _2_1: typeof mySymbol) => Type<true | ... 1 more ... | 1337, {}> (+2 overloads)
type("===", 1337, true, const mySymbol: typeof mySymbol
mySymbol)
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 const specialString: Type<string>
specialString = const type: TypeParser
<"string", Type<string, {}>>(def: "string") => Type<string, {}> (+2 overloads)
type("string").configure({
description: "a special string"
})
// sugar for adding description metadata
const const specialNumber: Type<number>
specialNumber = const type: TypeParser
<"number", Type<number, {}>>(def: "number") => Type<number, {}> (+2 overloads)
type("number").describe("a special number")
// this validator's error message will now start with "must be a special string"
const const specialString: Type<string>
specialString = const type: TypeParser
<readonly ["string", "@", {
readonly description: "a special string";
}], Type<string, {}>>(def: readonly ["string", "@", MetaSchema]) => Type<string, {}> (+2 overloads)
type([
"string",
"@",
{
description: "a special string"
}
])
// sugar for adding description metadata
const const specialNumber: Type<number>
specialNumber = const type: TypeParser
<readonly ["number", "@", "a special number"], Type<number, {}>>(def: readonly ["number", "@", MetaSchema]) => Type<number, {}> (+2 overloads)
type(["number", "@", "a special number"])
// this validator's error message will now start with "must be a special string"
const const specialString: Type<string>
specialString = const type: TypeParser
<"string", "@", readonly [{
readonly description: "a special string";
}], Type<string, {}>>(_0: "string", _1: "@", _2_0: MetaSchema) => Type<string, {}> (+2 overloads)
type("string", "@", {
description: "a special string"
})
// sugar for adding description metadata
const const specialNumber: Type<number>
specialNumber = const type: TypeParser
<"number", "@", readonly ["a special number"], Type<number, {}>>(_0: "number", _1: "@", _2_0: MetaSchema) => Type<number, {}> (+2 overloads)
type("number", "@", "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 type AutocompletedString = "foo" | "bar" | (string & {})
AutocompletedString = "foo" | "bar" | (string & {})
const const myObject: Type<{
autocompletedString: AutocompletedString;
}>
myObject = const type: TypeParser
<{
readonly autocompletedString: type.cast<AutocompletedString>;
}, Type<{
autocompletedString: AutocompletedString;
}, {}>>(def: validateObjectLiteral<{
readonly autocompletedString: type.cast<AutocompletedString>;
}, {}, bindThis<...>>) => Type<...> (+2 overloads)
type({
autocompletedString: "string" as type.cast<type AutocompletedString = "foo" | "bar" | (string & {})
AutocompletedString>
})
// allow any string, but suggest "foo" and "bar"
type type AutocompletedString = "foo" | "bar" | (string & {})
AutocompletedString = "foo" | "bar" | (string & {})
const const myObject: Type<{
autocompletedString: AutocompletedString;
}>
myObject = const type: TypeParser
<{
readonly autocompletedString: Type<AutocompletedString, {}>;
}, Type<{
autocompletedString: AutocompletedString;
}, {}>>(def: validateObjectLiteral<...>) => Type<...> (+2 overloads)
type({
autocompletedString: type.string.as<type AutocompletedString = "foo" | "bar" | (string & {})
AutocompletedString>()
})
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 const groups: Type<{
stringOrArrayOfNumbers: string | number[];
arrayOfStringsOrNumbers: (string | number)[];
}>
groups = const type: TypeParser
<{
readonly stringOrArrayOfNumbers: "string | number[]";
readonly arrayOfStringsOrNumbers: "(string | number)[]";
}, Type<{
stringOrArrayOfNumbers: string | number[];
arrayOfStringsOrNumbers: (string | number)[];
}, {}>>(def: validateObjectLiteral<...>) => Type<...> (+2 overloads)
type({
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 const disappointingGift: Type<{
label: string;
box?: ...;
}>
disappointingGift = const type: TypeParser
<{
readonly label: "string";
readonly "box?": "this";
}, Type<{
label: string;
box?: ...;
}, {}>>(def: validateObjectLiteral<{
readonly label: "string";
readonly "box?": "this";
}, {}, bindThis<{
readonly label: "string";
readonly "box?": "this";
}>>) => Type<...> (+2 overloads)
type({ label: "string", "box?": "this" })
const const myGift: {
label: string;
box?: ...;
}
myGift = const disappointingGift: Type<{
label: string;
box?: ...;
}>
disappointingGift.assert(await const fetchGift: () => Promise<null>
fetchGift())
// hover me
const const chainable: string | undefined
chainable = const myGift: {
label: string;
box?: ...;
}
myGift.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 const disappointingGifts: Type<{
label: string;
boxes: ...[];
}[]>
disappointingGifts = const type: TypeParser
<{
readonly label: "string";
readonly boxes: "this";
}, "[]", readonly [], Type<{
label: string;
boxes: ...[];
}[], {}>>(_0: validateObjectLiteral<{
readonly label: "string";
readonly boxes: "this";
}, {}, bindThis<{
readonly label: "string";
readonly boxes: "this";
}>>, _1: "[]") => Type<...> (+2 overloads)
type({ 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 const types: Module<{
disappointingGift: {
label: string;
box?: ...;
};
}>
types = const scope: ScopeParser
<{
readonly disappointingGift: {
readonly label: "string";
readonly "box?": "disappointingGift";
};
}>(def: scope.validate<{
readonly disappointingGift: {
readonly label: "string";
readonly "box?": "disappointingGift";
};
}>, config?: ArkScopeConfig) => Scope<...>
scope({
disappointingGift: {
label: "string",
// Resolves correctly to the root of the current type
"box?": "disappointingGift"
}
}).export()