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.
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.
Don't wrap your scoped definitions in type!
Even if you reference it as part of your scope definition, the global 'type' parser only knows about built-in keywords.
If you need access to fluent syntax from within a Scope, see thunks.
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.
.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.
If you don't plan to reuse your Scope to create additional types, it is common to export it inline:
type.module is available as sugar for this pattern:
Cyclic Types
Scopes make it easy to create recursive Types. Just reference the alias like you would any other:
Cyclic types are inferred to arbitrary depth. At runtime, they can safely validate cyclic data.
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:
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.
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:
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:
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:
thunks
When users are first learning about Scopes, one of the most common mistakes is to reference an alias in a nested type call:
This error occurs because although the id alias would be resolvable in the current Scope directly, type only allows references to built-in keywords. In this case, the type wrapper is redundant and the fix is to simply remove it:
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:
Though thunk definitions are really only useful when defining a Scope, they can be used anywhere a Type definition is expected: