A functional schema validation library.
- Lightweight - My data validation library shouldn't be bigger than React.
- Tree-shakeable - I don't want to take a hit for functionality that I'm not using.
- Composable - I want to validate my data with tiny, single-purpose functions.
- Type-safe - I want my validations to enforce my TypeScript types.
import { schema, assert, isString, isBlank, isNumber } from "@stackup/validate";
const userSchema = schema({
name: assert(isString).then(refute(isBlank, "Can't be blank")),
age: assert(isNumber).then(assert(age => age >= 18, "Must be 18 or older"))
});
const result = await userSchema.validate(data);
if (result.valid) {
console.log(result.value);
} else {
console.log(result.errors);
}
Install the package from NPM:
$ yarn add @stackup/validate
An operator is a function that returns a new Validator
. You can chain
together multiple operators with then
:
assert(isString)
.then(refute(isBlank))
.then(map(value => value.trim()))
.validate("hello!")
.then(result => {
if (result.valid) {
console.log(result.value);
} else {
console.log(result.errors);
}
});
Describes an object's properties.
schema({
name: assert(isString),
age: assert(isNumber)
});
Produces an error if a condition is not met.
assert(isString, "must be a string");
Produces an error if a condition is met.
refute(isBlank, "can't be blank");
Transforms the current value to a new value.
map(value => value.trim());
Runs the given validator unless the value is undefined
.
optional(assert(isString));
Runs the given validator unless the value is null
.
nullable(assert(isString));
Runs the given validator unless the value is either null
or undefined
.
maybe(assert(isString));
Runs the given validator when the predicate is truthy.
when(isString, refute(isBlank));
Runs the given validator against each item in an array.
each(assert(isString));
Provide a default value to replace null
or undefined
values.
defaultTo(0);
Skip validation for this field.
schema({
name: pass(),
age: pass()
});
A Validator
represents a step in the validation sequence. You probably won't
create a validator directly, but you certainly could:
const blankValidator = new Validator(input => {
if (isBlank(value)) {
return Validator.reject("can't be blank");
} else {
return Validator.resolve(value);
}
});
Runs the validator against user input. This function returns a Promise.
const result = await validator.validate(data);
if (result.valid) {
console.log(result.value);
} else {
console.log(result.errors);
}
Adds another validator to the current validation chain. This method returns an entirely new validator.
validator.then(refute(isBlank));
Add or overwrite the fields that a schema validates.
const user = schema({
name: assert(isString)
});
const admin = user.extend({
role: assert(role => role === "admin")
});
A predicate is a function that takes a value and returns true or false.
const isBlank = value => {
return value.trim() === "";
};
This library only includes the most essential predicate functions, because you can find thousands of predicate functions in the NPM ecosystem. Here are a few examples:
Checks if the value is a string.
Checks if the value is a number.
Checks if the value is an object.
Checks if the value is boolean.
Checks if the value is undefined
.
Checks if the value is null
.
Checks if the value is null
or undefined
.
Checks if a string is blank.
This library assumes that all input is unknown
by default. That means, you'll need
to narrow types before calling certain functions.
Here's an example that would cause a compile-time error:
schema({
// Error: 'unknown' is not assignable to type 'string'
name: refute(isBlank)
});
This is by design. To address this, you'll need to narrow types by passing a type guard to assert
:
schema({
name: assert(isString).then(refute(isBlank))
});
This library includes a few type guards out of the box, but you can also write your own:
const isStringOrNumber = (value: unknown): value is string | number => {
return typeof value === "string" || typeof value === "number";
};
You can ensure that your validators enforce your types.
interface User {
name: string;
}
// Error: Property 'name' is missing in type '{}'
schema<unknown, User>({});
// Error: 'number' is not assignable to type 'string'
schema<unknown, User>({ name: assert(isNumber) });
You can also generate new types from validators that you've defined:
const userSchema = schema({
name: assert(isString),
age: assert(isNumber);
});
type User = InferType<typeof userSchema>;