Type API

NameSummaryNotes & Examples
$Scope

in which chained methods are parsed

infer

type of output this returns

🥸 inference-only property that will be undefined at runtime

const  = ("string").pipe(s => Number.parseInt(s))
type  = typeof .infer // number
inferIn

type of input this allows

🥸 inference-only property that will be undefined at runtime

const  = ("string").pipe(s => Number.parseInt(s))
type  = typeof .inferIn // string
json

internal JSON representation

toJsonSchema

generate a JSON Schema

meta

metadata like custom descriptions and error messages

✅ type

can be customized

for your project

description

human-readable English description

✅ works best for primitive values

const  = ("0 < number <= 100")
console.log(.description) // positive and at most 100
expression

syntax string similar to native TypeScript

✅ works well for both primitives and structures

const  = ({ coords: ["number", "number"] })
console.log(.expression) // { coords: [number, number] }
assert

validate and return transformed data or throw

✅ sugar to avoid checking for

type.errors

if they are unrecoverable

const  = ({
    superImportantValue: "string"
})
// throws TraversalError: superImportantValue must be a string (was missing)
const  = .assert({ irrelevantValue: "whoops" })
console.log(.superImportantValue) // valid output can be accessed directly
allows

check input without applying morphs

✅ good for stuff like filtering that doesn't benefit from detailed errors

const  = ("number | bigint")
// [0, 2n]
const  = [0, "one", 2n].filter(.allows)
configure

add metadata to shallow references

⚠️ does not affect error messages within properties of an object

const  = ("number % 2").configure({ description: "not odd" })
// all constraints at the root are affected
const  = NotOdd(3) // must be not odd (was 3)
const  = NotOdd("two") // must be not odd (was "two")

const  = ({
   // we should have referenced notOdd or added meta here
   notOdd: "number % 2",
// but instead chained from the root object
}).configure({ description: "not odd" })
// error message at path notOdd is not affected
const  = NotOddBox({ notOdd: 3 }) // notOdd must be even (was 3)
// error message at root is affected, leading to a misleading description
const  = NotOddBox(null) // must be not odd (was null)
describe

add description to shallow references

🔗 equivalent to .configure({ description }) (see

configure

)

⚠️ does not affect error messages within properties of an object

const  = (/^a.*z$/).describe("a string like 'a...z'")
const  = AToZ("alcatraz") // "alcatraz"
// ArkErrors: must be a string like 'a...z' (was "albatross")
const  = AToZ("albatross")
onUndeclaredKey

apply undeclared key behavior

"ignore" (default) - allow and preserve extra properties

"reject" - disallow extra properties

"delete" - clone and remove extra properties from output

onDeepUndeclaredKey

deeply apply undeclared key behavior

"ignore" (default) - allow and preserve extra properties

"reject" - disallow extra properties

"delete" - clone and remove extra properties from output

from

alias for

assert

with typed input

const  = ({ foo: "string" });
// TypeScript: foo must be a string (was 5)
const  = .from({ foo: 5 });
brand

add a compile-time brand to output

🥸 inference-only function that does nothing runtime

const  = ("string")
    .narrow(s => s === [...s].reverse().join(""))
    .brand("palindrome")
// Brand<string, "palindrome">
const  = .assert("racecar")
array

an array of this

// Type<{ rebmun: number }[]>
const  = ({ rebmun: "number" }).array();
optionaloptional definition

⚠️ unlike most other methods, this creates a definition rather than a Type (read why)

const  = ({ foo: "number" })
// Type<{ bar?: { foo: number } }>
const  = ({ bar: .optional() })
defaultdefaultable definition

✅ object defaults can be returned from a function

⚠️ throws if the default value is not allowed

⚠️ unlike most other methods, this creates a definition rather than a Type (read why)

// Type<{ count: Default<number, 0> }>
const  = ({ count: type.number.default(0) })
const  = ({ nested: "boolean" })
const  = ({
    key: .default(() => ({ nested: false }))
})
filter

apply a predicate function to input

⚠️ the behavior of

narrow

, this method's output counterpart, is usually more desirable

✅ most useful for morphs with input types that are re-used externally

🥸

Type predicates

can be used as casts

const  = ({ name: "string" }).pipe(user => JSON.stringify(user))
const  = .filter(user => user.name !== "Bobby Tables")
// Type<(In: `${string}Z`) => To<Date>>
const  = ("string.date.parse").filter((s): s is `${string}Z` =>
    s.endsWith("Z")
)
narrow

apply a predicate function to output

✅ go-to fallback for validation not composable via built-in types and operators

✅ runs after all other validators and morphs, if present

🥸

Type predicates

can be used as casts

const  = ("string").narrow(s => s === [...s].reverse().join(""))

const  = ("string.date.parse").narrow((date, ctx) =>
		date.getFullYear() === 2025 || ctx.mustBe("the current year")
)
// Type<`${string}.tsx`>
const  = ("string").narrow((s): s is `${string}.tsx` => /\.tsx?$/.test(s))
pipe

pipe output through arbitrary transformations or other Types

const  = ({ name: "string" })

// parse a string and validate that the result as a user
const  = ("string").pipe(s => JSON.parse(s), user)
to

parse a definition as an output validator

🔗 to({ name: "string" }) is equivalent to .pipe(type({ name: "string" }))

// parse a string and validate that the result as a user
const  = ("string").pipe(s => JSON.parse(s)).to({ name: "string" })
select

query internal node references

filters and returns the Type's internal representation from @ark/schema

// ["blue", "red"]
const  = ("'red' | 'blue'").select("unit").map(u => u.unit)
as

cast the way this is inferred

🥸 inference-only function that does nothing runtime

// Type<`LEEEEEEEE${string}ROY`>
const  = (/^LE{8,}ROY$/).as<`LEEEEEEEE${string}ROY`>()
and

intersect the parsed Type, throwing if the result is unsatisfiable

// Type<{ foo: number; bar: string }>
const  = ({ foo: "number" }).and({ bar: "string" })
// ParseError: Intersection at foo of number and string results in an unsatisfiable type
const  = ({ foo: "number" }).and({ foo: "string" })
or

union with the parsed Type

⚠️ a union that could apply different morphs to the same data is a ParseError (

docs

)

// Type<string | { box: string }>
const  = ("string").or({ box: "string" })
intersect

intersect the parsed Type, returning an introspectable

Disjoint

if the result is unsatisfiable

// Type<{ foo: number; bar: string }>
const  = ({ foo: "number" }).intersect({ bar: "string" })
const  = ("number > 10").intersect("number < 5")
// logs "Intersection of > 10 and < 5 results in an unsatisfiable type"
if ( instanceof Disjoint) console.log(`${bad.summary}`)
equals

check if the parsed Type's constraints are identical

✅ equal types have identical input and output constraints and transforms

✅ ignores associated

meta

, which does not affect the set of allowed values

const  = type.number.divisibleBy(6).moreThan(0)
// false (left side must also be positive)
.equals("number % 6")
// false (right side has an additional <100 constraint)
console.log(.equals("0 < (number % 6) < 100"))
const  = ("(number % 2) > 0").divisibleBy(3)
// true (types are normalized and reduced)
console.log(.equals())
ifEquals

narrow this based on an

equals

check

✅ ignores associated

meta

, which does not affect the set of allowed values

const  = type.raw(`${Math.random()}`)
// Type<0.5> | undefined
const  = .ifEquals("0.5")
extends

check if this is a subtype of the parsed Type

✅ a subtype must include all constraints from the base type

✅ unlike

equals

, additional constraints may be present

✅ ignores associated

meta

, which does not affect the set of allowed values

type.string.extends("unknown") // true
type.string.extends(/^a.*z$/) // false
ifExtends

narrow this based on an

extends

check

✅ ignores associated

meta

, which does not affect the set of allowed values

const  = (Math.random() > 0.5 ? "true" : "0") // Type<0 | true>
const  = .ifExtends("boolean") // Type<true> | undefined
overlaps

check if a value could satisfy this and the parsed Type

⚠️ will return true unless a

Disjoint

can be proven

type.string.overlaps("string | number") // true (e.g. "foo")
("string | number").overlaps("1") // true (1)
("number > 0").overlaps("number < 0") // false (no values exist)

const  = ("string").narrow(s => !s.includes("@"))
.overlaps("string.email") // true (no values exist, but not provable)
extract

extract branches

extend

ing the parsed Type

// Type<true | 0 | 2>
const  = ("boolean | 0 | 'one' | 2 | bigint").extract("number | 0n | true")
exclude

exclude branches

extend

ing the parsed Type

// Type<false | 'one' | bigint>
const  = ("boolean | 0 | 'one' | 2 | bigint").exclude("number | 0n | true")

The methods below are available on every Type instance. For validation and traversal methods (assert, allows, direct invocation), see the Traversal API. For composition methods (pipe, to, narrow, filter, and, or), see Expressions. For object-specific methods (pick, omit, required, partial, merge, keyof, get, readonly, map, props), see Objects.

from

from is a typed-input variant of assert. It accepts an input matching inferIn and returns inferOut, providing type safety on both sides. Like assert, it throws a TraversalError on invalid input:

const  = ("string.numeric.parse")

// TypeScript knows the input must be a string
const  = .from("42") // 42

.from(42)
Argument of type 'number' is not assignable to parameter of type 'string'.

in / out

The in and out getters extract the input or output Type from a morphed Type, stripping transformations:

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

// Type<{ name: string; age: string }>
const  = .in

// Type<{ name: string; age: number }>
const  = .out

extends

Check if a Type is a subtype of another:

const  = ("string")

.extends("unknown") // true
.extends("number") // false

// ifExtends returns the Type itself if true, undefined otherwise
const  = .ifExtends("string | number") // Type<string | number> | undefined

equals

Check if two Types are structurally identical:

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

.equals() // true
.equals() // false

// ifEquals returns the Type if equal, undefined otherwise
const  = .ifEquals() // Type<{ name: string }> | undefined

overlaps

Check if any value could satisfy both Types:

const  = ("string | number")
const  = ("number | boolean")
const  = ("string")

.overlaps() // true (number satisfies both)
.overlaps(("number")) // false

extract / exclude

Extract or exclude union branches based on a Type:

const  = ("string | number | boolean")

// Type<string | boolean>
const  = .extract("string | boolean")

// Type<number>
const  = .exclude("string | boolean")

distribute

Map and optionally reduce over union branches:

const  = ("string | number | bigint")

// ["bigint", "number", "string"]
const  = .distribute(branch => branch.expression)

// with a reducer
const  = .distribute(
	branch => branch,
	branches => branches.length
) // 3

toJsonSchema

Each Type instance exposes a toJsonSchema() method that can be used to generate a corresponding JSON Schema.

const  = ({
	name: "string",
	email: "string.email",
	"age?": "number >= 18"
})

const  = .toJsonSchema()

const  = {
	$schema: "https://json-schema.org/draft/2020-12/schema",
	type: "object",
	properties: {
		name: { type: "string" },
		email: {
			type: "string",
			format: "email",
			pattern: "^[\w%+.-]+@[\d.A-Za-z-]+\.[A-Za-z]{2,}$"
		},
		age: { type: "number", minimum: 18 }
	},
	required: ["email", "name"]
}

Options can be passed to change the behavior including how incompatibilities are handled. See the associated config docs for more details.