Introspection

Types have an extremely powerful internal representation defined in @ark/schema that is primarily exposed through the .internal property on each Type.

Though APIs under .internal are not officially frozen, they are stable enough that we want to start giving users more direct access to some of the introspection capabilities they provide.

Node kinds

All nodes have a kind property indicating their purpose, structure and special properties.

Roots

The kind at the root of a Type will always be one of the following root kind.

Bases

The simplest root nodes are defined by a single basis constraint.

Only a single basis can exist in an intersection. From widest to narrowest:

kinddescriptionexample
domain

One of 5 non-enumerable type sets (string, number, object, bigint, symbol)

{ domain: "string" }
proto

A constructor checked by instanceof (implies domain object)

{ proto: Date }
unit

An exact value checked by === (can be intersected with any other constraint and reduced to itself or never)

{ unit: true }
Composites

Root kinds are built from references to other nodes.

Will be normalized to appear in approximately the following hierarchical order:

kinddescriptionexample
aliasStores a cyclic reference to a node{ reference: "$name" }
union

A set of allowed nodes

{ branches: ["string", "Array"] }
morph

One or more transformations applied to valid data

{ in: "string", morphs: [(s) => s.trim()] }
intersectionAn intersection of constraints{ domain: "number", divisor: 5 }

Constraints

Constraint nodes exist on an intersection (or its structure) and narrow the set of values allowed by its basis.

Refinements

Constraints that apply directly to the root of an intersection (includes the base structure node but not its children).

kindimpliedBasisdescriptionexample
divisornumberMultiple of the specified integer{ rule: 2 }
patternstringMatched by a regex{ rule: "^[a-z]+$" }
minnumberNumeric minimum (inclusive by default){ rule: 0, exclusive: true }
maxnumberNumeric maximum (inclusive by default){ rule: 100 }
minLengthstring | ArrayInclusive minimum length{ rule: 1 }
maxLengthstring | ArrayInclusive maximum length{ rule: 255 }
exactLengthstring | ArrayExact length{ rule: 10 }
afterDateMinimum Date (inclusive by default){ rule: new Date("2000-01-01") }
beforeDateMaximum Date (inclusive by default){ rule: new Date() }
predicateunknownCustom narrow function{ predicate: (n) => n % 2 === 1 }
Structural

These constraints refine a structure node, defining the shape of properties and/or array elements.

kinddescriptionexample
sequenceArray/tuple shape{ prefix: ["string"], variadic: "number" }
requiredRequired object property{ key: "id", value: "number" }
optionalOptional object property{ key: "name", value: "string" }
indexProperties allowed by signature must conform to value{ signature: "string", value: "boolean" }

More details on the type system to come!

select

select is not fully stable!

select relies on the internal representation defined in @ark/schema, which although relatively mature, is not guaranteed semver-stable.

select is the top-level first method we're introducing for interacting with a Type based on its internal representation.

It can be used to filter a Type's references:

const  = ({
	name: "string > 5",
	flag: "0 | 1"
})
	.array()
	.atLeastLength(1)

// get all references representing literal values
const  = .select("unit") // [Type<0>, Type<1>]

// get all references representing literal positive numbers
const  = .select({
	kind: "unit",
	where: u => typeof u.unit === "number" && u.unit > 0
}) // [Type<1>]

// get all minLength constraints at the root of the Type
const  = .select({
	kind: "minLength",
	// the shallow filter excludes the constraint on `name`
	boundary: "shallow"
}) // [MinLengthNode<1>]

This can be used directly or in combination with the configure API for fine-grained control over which nodes to modify.

On this page