@minstack/schema
v1.0.3
Published
Composable [type predicates](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) for runtime type checking.
Downloads
1
Maintainers
Readme
Schema
Composable type predicates for runtime type checking.
Getting Started
Import the schema namespace. Using $
is recommended for brevity.
import * as $ from './schema.js';
// Or, as an NPM package.
import * as $ from '@minstack/schema';
Construct a new custom schema from pre-existing schemas.
const isPerson = $.object({
name: $.string(),
age: $.number(),
});
Infer the type from the custom schema (if required).
type Person = $.SchemaType<typeof isPerson>;
Use the custom schema to narrow the type of a variable.
if (isPerson(value)) {
// Value type is narrowed to: Person
}
Included Schemas
Simple schemas match the basic TS types.
string()
number()
bigint()
boolean()
symbol()
callable()
- Match functions or constructors.notDefined()
defined
nul()
notNul()
nil()
- Match null or undefined.notNil()
any()
- Match anything as typeany
.unknown()
- Match anything as typeunknown
.
Configurable schemas accept values for matching.
enumeration(enumType: EnumLike)
literal(...primitives: Primitive[])
instance(...constructors: AnyConstructor[])
Composition schemas merge other schemas (or predicates) into more complex schemas.
union(...predicates: AnyPredicate[])
intersection(...predicates: AnyPredicate[])
object(shape: Record<string, AnyPredicate>)
tuple(...shape: AnyPredicate[])
record(type?: AnyPredicate)
array(type?: AnyPredicate)
Utilities which are less commonly used, or normally only used internally.
schema<T>(predicate: (value: unknown) => value is T)
- Create a custom schema.
predicate<T>(predicate: (value: unknown) => value is T)
- Create a predicate (copy).
lazy(resolve: () => AnyPredicate)
- Delay resolving a predicate until needed (for recursive types).
assert(predicate: AnyPredicate, value: unknown, error?: ErrorLike)
- Throw if the predicate does not match the value.
Custom Schema
Use the schema
utility to create custom schemas with arbitrary validation logic. Creating a factory function which returns a new schema is recommended.
const isNumericString = () => {
return $.schema<`${number}`>((value) => {
return (
typeof value === 'string' &&
value.trim() !== '' &&
!Number.isNaN(value as unknown as number);
});
};
Use the custom schema like any other schema.
const isNumeric = $.union($.number(), isNumericString());
if (isNumeric(value)) {
// Value type is narrowed to: number | `${number}`
}
Extension Methods
All schemas have basic extension methods.
.or(predicate: AnyPredicate)
- Union.
.and(predicate: AnyPredicate)
- Intersection.
.optional()
- Union with
undefined
.
- Union with
An optional string schema could be created as follows.
const isOptionalString = $.string().optional();
All collection schemas (object
, tuple
, record
, array
) have additional extension methods.
.partial()
- Make all entries optional.
.required()
- Make all entries required.
The array
schema has an additional extension method.
.nonEmpty()
- Match arrays with length > 0.
The object
schema has an additional extension method.
.extend(shape: Record<string, AnyPredicate>)
- Add new properties or additional constraints (intersections) on existing properties.
Type Assertions
It can be useful to throw an error when a predicate does not match a value.
$.assert($.string(), value, 'value is not a string');
After the assert, the type of value
will be narrowed to the predicate type.
The above assertion is equivalent to the following conditional throw.
if (!$.string()(value)) {
throw new TypeError('value is not a string');
}
Recursive Types
Recursive (self referential) types are possible, but require a few extra steps because type inference doesn't handle self references (Errors: TS7022, TS2454).
First, define the non-recursive part of the schema.
const isNode = $.object({ name: $.string() });
Then, derive the recursive type. This type is needed to explicitly type the recursive schema.
type Node = $.SchemaType<typeof isNode>;
type Tree = Node & { children: Tree[] };
And finally, extend the non-recursive schema to add recursion, and assign the final schema to an explicitly typed variable.
const isTree: $.ObjectSchema<Tree> = isNode.extend({
children: $.array($.lazy(() => isTree)),
});
Wrong ways to make recursive types
A TS2454
error is raised without the $.lazy
wrapper around the self reference. This is because the schema is used before it is defined.
const isTree: $.ObjectSchema<Tree> = isNode.extend({
// Error: Variable 'isTree' is used before being assigned. ts(2454)
children: $.array(isTree),
});
A TS7022
error is raised when defining the recursive type in a single step. This is because typescript cannot automatically infer a type which references itself.
// Error: 'isTree' implicitly has type 'any' because it does not have a
// type annotation and is referenced directly or indirectly in its
// own initializer. ts(7022)
const isTree = $.object({
name: $.string(),
children: $.array($.lazy(() => isTree)),
});