Scopes

Scopes are the foundation of ArkType, and one of the most powerful features for users wanting full control over configuration and to make their own keywords available fluidly within string definition syntax.

A scope is just like a scope in code- a resolution space where you can define types, generics, or other scopes. The type export is a actually just a method on our default Scope!

Defining a Scope

To define a scope, you may either import { scope } from "arktype" or use type.scope on the default type export.

A scope is specified as an object literal mapping names to definitions.

import { type } from "arktype"

const  = ({
	// keywords are still available in your scope
	id: "string",
	// but you can also reference your own aliases directly!
	user: { id: "id", friends: "id[]" },
	// your aliases will be autocompleted and validated alongside ArkType's keywords
	usersById: {
		"[id]": "user | undefined"
	}
})

coolScope is an object with reusable methods like type and generic. You can use it to create additional Types that can reference your aliases- id, user and usersById.


const  = .type({
	name: "string",
	members: "user[]"
})

// chained definitions are parsed in the same scope as the original Type
const  = .and({
	ownerId: "id"
})

To use the scoped types directly, you must .export() your Scope to a Module. A Module is just an object mapping aliases to Types. They can be used for validation or in any other context a Type can be used.


const  = .export()

const  = .user({
	id: "99",
	friends: ["7", 8, "9"]
})

if ( instanceof type.errors) {
	// hover summary to see validation errors
	console.error(.)
}

.export() is also useful in combination with the spread operator for extending your Scopes. Recall that a Type can be referenced as a definition. This means that spreading a Module into the definition you pass to scope includes all of that Module's aliases in your new Scope.


const  = ({
	three: "3",
	sixty: "60",
	no: "'no'"
})

const  = ({
	....export(),
	// if you don't want to include the entire scope, you can pass a list of ...aliases
	....export("three", "sixty"),
	saiyan: {
		powerLevel: "number > 9000"
	}
})

If you don't plan to reuse your Scope to create additional types, it is common to export it inline:

const  = ({
	ez: "'moochi'"
}).export()

type.module is available as sugar for this pattern:

const  = type.module({
	ez: "'moochi'"
})

Cyclic Types

Scopes make it easy to create recursive Types. Just reference the alias like you would any other:

export const  = ({
	package: {
		name: "string",
		"dependencies?": "package[]",
		"contributors?": "contributor[]"
	},
	contributor: {
		email: "string.email",
		"packages?": "package[]"
	}
}).export()

Cyclic types are inferred to arbitrary depth. At runtime, they can safely validate cyclic data.


export type  = typeof .package.infer

const :  = {
	name: "arktype",
	dependencies: [{ name: "typescript" }],
	contributors: [{ email: "david@sharktypeio" }]
}

// update arktype to depend on itself
.dependencies![0].dependencies = []

// ArkErrors: contributors[0].email must be an email address (was "david@sharktypeio")
const  = .package()

Some `any`s are not what they seem!

By default, TypeScript represents anonymous cycles as .... However, if you have noErrorTruncation enabled, they are visually displayed as any😬

Luckily, despite its appearance, the type otherwise behaves as you'd expect- TypeScript will provide completions and will complain as normal if you access a non-existent property.

visibility

Intermediate aliases can be useful for composing Scoped definitions from aliases. Sometimes, you may not want to expose those aliases externally as Types when your Scope is exported.

This can be done using private aliases:

const  = ({
	// aliases with a "#" prefix are treated as private
	"#baseShapeProps": {
		perimeter: "number",
		area: "number"
	},
	ellipse: {
		// when referencing a private alias, the "#" should not be included
		"...": "baseShapeProps",
		radii: ["number", "number"]
	},
	rectangle: {
		"...": "baseShapeProps",
		width: "number",
		height: "number"
	}
})

// private aliases can be referenced from any scoped definition,
// even outside the original scope
const  = .type("Partial<baseShapeProps>")

// when the scope is exported to a Module, they will not be included
// hover to see the Scope's exports
const  = .export()

import()

Private aliases are especially useful for building scopes without polluting them with every alias you might want to reference internally. To facilitate this, Scopes have an import() method that behaves identically to export() but converts all exported aliases to private.

const  = ({
	"withId<o extends object>": {
		"...": "o",
		id: "string"
	}
})

const  = type.module({
	// because we use `import()` here, we can reference our utilities
	// internally, but they will not be included in `userModule`.
	// if we used `export()` instead, `withId` could be accessed on `userModule`.
	....import(),
	payload: {
		name: "string",
		age: "number"
	},
	db: "withId<payload>"
})

submodules

If you've used keywords like string.email or number.integer, you may wonder if aliases can be grouped in your own Scopes. Recall from the introduction to Scopes that type is actually just a method on ArkType's default Scope, meaning all of its functionality is available externally, including alias groups called Submodules.

Submodules are groups of aliases with a shared prefix. To define one, just assign the value of the prefix to a Module with the names you want:

const  = type.module({ alias: "number" })

const  = ({
	a: "string",
	b: "sub.alias",
	sub: 
})

const  = .type({
	someKey: "sub.alias[]"
})

Submodules are parsed bottom-up. This means subaliases can be referenced directly in the root scope, but root aliases can't be referenced from the submodule, even if it's inlined.

nested

Submodules can be nested to arbitrary depth:


const  = ({
	// reference rootScope from our previous example
	newRoot: .export()
})

const  = .type({
	someOtherKey: "newRoot.sub.alias | boolean"
})

rooted

The Submodules from our previous examples group Types together, but cannot be referenced as Types themselves the way string and number can. To define a Rooted Submodule, just use an alias called root:

const  = type.module({
	root: {
		name: "string"
	},
	// subaliases can extend a base type by referencing 'root'
	// like any other alias
	admin: {
		"...": "root",
		isAdmin: "true"
	},
	saiyan: {
		"...": "root",
		powerLevel: "number > 9000"
	}
})

const  = type.module({
	user: ,
	// user can now be referenced directly in a definition
	group: "user[]",
	// or used as a prefix to access subaliases
	elevatedUser: "user.admin | user.saiyan"
})

thunks

When users are first learning about Scopes, one of the most common mistakes is to reference an alias in a nested type call:

const  = scope({
	id: "string#id",
	user: ({
		name: "string",
		id: "id"
TypeScript: 'id' is unresolvable 
}) })

This error occurs because although the id alias would be resolvable in the current Scope directly, type only allows references to builtin keywords. In this case, the type wrapper is redundant and the fix is to simply remove it:

const  = ({
	id: "string#id",
	user: {
		name: "string",
		// now resolves correctly
		id: "id"
	}
})

However, even if it is possible to define your scope without invoking type by composing aliases and tuple expressions, the fluent methods available on Type can define complex types that can be cumbersome to express otherwise. In these situations, you can use a thunk definition to access the type method on the Scope you're currently defining:

const  = ({
	id: "string#id",
	expandUserGroup: () =>
		.type({
			name: "string",
			id: "id"
		})
			.or("id")
			.pipe(user =>
				typeof user === "string" ? { id: user, name: "Anonymous" } : user
			)
			.array()
			.atLeastLength(2)
})

const  = .export()

// input is validated and transformed to:
// [{ name: "Magical Crawdad", id: "777" }, { name: "Anonymous", id: "778" }]
const  = .expandUserGroup([
	{ name: "Magical Crawdad", id: "777" },
	"778"
])

Though thunk definitions are really only useful when defining a Scope, they can be used anywhere a Type definition is expected:

// you *can* use them anywhere, but *should* you? (no)
const  = (() =>
	({ inelegantKey: () => ("'inelegant value'") })
)

On this page