spectypes
June 18, 2022 ยท View on GitHub
Fast, compiled, eval-free data validator/transformer
Features
- really fast, can be even faster than
ajv - detailed errors, failure will result into explicit error messages and path to invalid data
- extensively tested, each release undergoes more than 900
fast-checkpowered tests - precise types, accurately infers all types and provides readable compile-time error messages
- browser friendly, uses
babelto compile validators, so noevalornew Functioninvolved - easily extensible, custom validators are created by mixing existing ones
Getting started
-
There are two packages to install -
spectypes, which contains type definitions and small set of runtime helpers andbabel-plugin-spectypes, which parses and compiles validators into functions:npm i spectypes npm i babel-plugin-spectypes -D -
Add
babel-plugin-spectypesto plugins section in yourbabelconfig:"plugins": [ + "babel-plugin-spectypes" ]
Example
Original code:
import { array, number } from 'spectypes'
const check = array(number)
The plugin will search for named imports like import { ... } from 'spectypes' or const { ... } = require('spectypes') and get all imported identifiers (aliases also supported). All variable declarations which include these identifiers will be converted into validating functions.
Transformed code:
const check = (value) => {
let err
if (!Array.isArray(value)) {
;(err = err || []).push({
issue: 'not an array',
path: []
})
} else {
for (let index = 0; index < value.length; index++) {
const value_index = value[index]
if (typeof value_index !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: [index]
})
}
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
Reference
Primitive validators
Complex validators
Utilities
Primitive validators
boolean
Validates a boolean value
import { boolean } from 'spectypes'
const check = boolean
expect(check(true)).toEqual({
tag: 'success',
success: true
})
expect(check('false')).toEqual({
tag: 'failure',
failure: {
value: 'false',
errors: [{ issue: 'not a boolean', path: [] }]
}
})
Transformed code
|
literal
Creates a literal validator spec. literalcan validate strings, numbers, booleans, undefined and null. literal(undefined) is treated specially when used as a property validator inside object or struct.
import { literal } from 'spectypes'
const check = literal('test')
expect(check('test')).toEqual({
tag: 'success',
success: 'test'
})
expect(check('temp')).toEqual({
tag: 'failure',
failure: {
value: 'temp',
errors: [{ issue: "not a 'test' string literal", path: [] }]
}
})
Transformed code
|
nullish
Transformer spec, that accepts undefined and null values and maps them to undefined.
nullish is treated specially when used as a property validator inside object or struct.
import { nullish } from 'spectypes'
const check = nullish
expect(check(undefined)).toEqual({
tag: 'success'
success: undefined
})
expect(check(null)).toEqual({
tag: 'success'
success: undefined
})
expect(check(123)).toEqual({
tag: 'failure',
failure: {
value: 'temp',
errors: [{ issue: "not 'null' or 'undefined'", path: [] }]
}
})
Transformed code
|
number
Validates a number value.
import { number } from 'spectypes'
const check = number
expect(check(0)).toEqual({
tag: 'success',
success: 0
})
expect(check({})).toEqual({
tag: 'failure',
failure: {
value: {},
errors: [{ issue: 'not a number', path: [] }]
}
})
Transformed code
|
string
Validates a string value.
import { string } from 'spectypes'
const check = string
expect(check('')).toEqual({
tag: 'success',
success: ''
})
expect(check(null)).toEqual({
tag: 'failure',
failure: {
value: null,
errors: [{ issue: 'not a string', path: [] }]
}
})
Transformed code
|
unknown
Empty validator spec. unknown is treated specially when used as a property validator inside object or struct.
import { unknown } from 'spectypes'
const check = unknown
expect(check('anything')).toEqual({
tag: 'success',
success: 'anything'
})
Transformed code
|
Complex validators
array
Creates an array validator spec. Takes a spec to validate each item of an array.
import { array, number } from 'spectypes'
const check = array(number)
expect(check([1, 2, 3])).toEqual({
tag: 'success',
success: [1, 2, 3]
})
expect(check({ 0: 1 })).toEqual({
tag: 'failure',
failure: {
value: { 0: 1 },
errors: [{ issue: 'not an array', path: [] }]
}
})
expect(check([1, 2, '3', false])).toEqual({
tag: 'failure',
failure: {
value: [1, 2, '3', false],
errors: [
{ issue: 'not a number', path: [2] },
{ issue: 'not a number', path: [3] }
]
}
})
Transformed code
|
filter
Can be used only as an argument for array and record to create filtered transformer specs. Filtering happens after each item or key validation. Takes a spec to validate each item or key of a collection and filter predicate.
import { array, number, filter } from 'spectypes'
const check = array(filter(number, (x) => x > 1))
expect(check([1, 2, 3])).toEqual({
tag: 'success',
success: [2, 3]
})
expect(check([1, 2, null])).toEqual({
tag: 'failure',
failure: {
value: [1, 2, null],
errors: [{ issue: 'not a number', path: [2] }]
}
})
Transformed code
|
Type predicate will be taken into account if provided
import { array, string, filter } from 'spectypes'
const check = array(filter(string, (x): x is 'test' => x === 'test'))
expect(check(['hello', 'test', 'world'])).toEqual({
tag: 'success',
success: ['test'] // readonly 'test'[]
})
limit
Creates a spec with custom constraint. Takes a basis spec and a function to perform additinal validation.
import { number, limit } from 'spectypes'
const check = limit(number, (x) => x > 1)
expect(check(5)).toEqual({
tag: 'success',
success: 5
})
expect(check(-5)).toEqual({
tag: 'failure',
failure: {
value: -5,
errors: [{ issue: 'does not fit the limit', path: [] }]
}
})
expect(check('5')).toEqual({
tag: 'failure',
failure: {
value: '5',
errors: [{ issue: 'not a number', path: [] }]
}
})
Transformed code
|
Type predicate will be taken into account if provided
import { array, string, limit } from 'spectypes'
const check = array(limit(string, (x): x is 'test' => x === 'test'))
expect(check(['test', 'test', 'test'])).toEqual({
tag: 'success',
success: ['test', 'test', 'test'] // readonly 'test'[]
})
map
Creates a spec that transforms the result of successful validation. Takes basis spec and mapping function.
import { number, map } from 'spectypes'
const check = map(number, (x) => x + 1)
expect(check(10)).toEqual({
tag: 'success',
success: 11
})
expect(check(undefined)).toEqual({
tag: 'failure',
failure: {
value: undefined,
errors: [{ issue: 'not a number', path: [] }]
}
})
Transformed code
|
merge
Can combine tuple with array or object with record into single spec.
import { tuple, array, string, boolean, merge } from 'spectypes'
const check = merge(tuple(string, string), array(boolean))
expect(check(['hello', 'world', true])).toEqual({
tag: 'success',
success: ['hello', 'world', true]
})
expect(check(['hello', 'world', '!'])).toEqual({
tag: 'failure',
failure: {
value: ['hello', 'world', '!'],
errors: [{ issue: 'not a string', path: [2] }]
}
})
expect(check(['hello'])).toEqual({
tag: 'failure',
failure: {
value: ['hello'],
errors: [{ issue: 'length is less than 2', path: [] }]
}
})
Transformed code
|
import { object, record, number, string, boolean, merge } from 'spectypes'
const check = merge(object({ x: number }), record(string, boolean))
expect(check({ x: 123, y: true })).toEqual({
tag: 'success',
success: { x: 123, y: true }
})
expect(check({ x: true, y: 123 })).toEqual({
tag: 'failure',
failure: {
value: { x: true, y: 123 },
errors: [
{ issue: 'not a number', path: ['x'] },
{ issue: 'not a boolean', path: ['y'] }
]
}
})
Transformed code
|
object
Creates an object validator spec. Validation will fail if validated object has a property set different from the one specified. Takes an object with specs to validate object properties. literal(undefined), nullish and unknown are treated specially when used as a property validator inside object.
import { object, number, string, boolean } from 'spectypes'
const check = object({ x: number, y: string, z: boolean })
expect(check({ x: 1, y: '2', z: false })).toEqual({
tag: 'success',
success: { x: 1, y: '2', z: false }
})
expect(check({ x: 1, y: '2', z: false, xyz: [] })).toEqual({
tag: 'failure',
failure: {
value: { x: 1, y: '2', z: false, xyz: [] },
errors: [{ issue: 'excess key - xyz', path: [] }]
}
})
expect(check({})).toEqual({
tag: 'failure',
failure: {
value: {},
errors: [
{ issue: 'not a number', path: ['x'] },
{ issue: 'not a string', path: ['y'] },
{ issue: 'not a boolean', path: ['z'] }
]
}
})
expect(check([])).toEqual({
tag: 'failure',
failure: {
value: [],
errors: [{ issue: 'not an object', path: [] }]
}
})
Transformed code
|
optional
Creates an optional object property validator spec. Can be used only inside object and struct arguments. Will not produce any validation errors if property equals undefined or is not present in the validated object.
import { optional, struct, number } from 'spectypes'
const check = struct({ x: optional(number) })
expect(check({ x: 5 })).toEqual({
tag: 'success',
success: { x: 5 }
})
expect(check({ x: undefined })).toEqual({
tag: 'success',
success: { x: undefined }
})
expect(check({})).toEqual({
tag: 'success',
success: {}
})
expect(check({ x: 'x' })).toEqual({
tag: 'failure',
failure: {
value: { x: 'x' },
errors: [{ issue: 'not a number', path: ['x'] }]
}
})
Transformed code
|
record
Creates a record validator spec. This validator is protected from prototype pollution and validation will fail if validated object contains properties that override Object.proptotype methods. This function has two signatures - one takes a spec to validate each key of a record and a spec to validate each item, another takes only item spec and treats all keys as strings. Key spec can be a string, template, string literal or union of these specs.
import { record, boolean } from 'spectypes'
const check = record(boolean)
expect(check({ foo: false, bar: true })).toEqual({
tag: 'success',
success: { foo: false, bar: true }
})
expect(check(true)).toEqual({
tag: 'failure',
failure: {
value: true,
errors: [{ issue: 'not an object', path: [] }]
}
})
expect(check({ toString: true })).toEqual({
tag: 'failure',
failure: {
value: { toString: true },
errors: [{ issue: "includes banned 'toString' key", path: [] }]
}
})
Transformed code
|
struct
Creates an object transformer spec. All properties of validated object that are not present in passed param will be removed from the result of successful validation. Takes an object with specs to validate object properties. literal(undefined), nullish and unknown are treated specially when used as a property validator inside struct.
import { struct, number, string, boolean } from 'spectypes'
const check = struct({ x: number, y: string, z: boolean })
expect(check({ x: 1, y: '2', z: false })).toEqual({
tag: 'success',
success: { x: 1, y: '2', z: false }
})
expect(check({ x: 1, y: '2', z: false, xyz: [] })).toEqual({
tag: 'success',
success: { x: 1, y: '2', z: false }
})
expect(check({})).toEqual({
tag: 'failure',
failure: {
value: {},
errors: [
{ issue: 'not a number', path: ['x'] },
{ issue: 'not a string', path: ['y'] },
{ issue: 'not a boolean', path: ['z'] }
]
}
})
expect(check([])).toEqual({
tag: 'failure',
failure: {
value: [],
errors: [{ issue: 'not an object', path: [] }]
}
})
Transformed code
|
template
Creates a template string validator spec. Takes number, string, boolean, literal specs and their unions to validate parts of the validated string.
import { template, literal, number, string, boolean } from 'spectypes'
const check = template(literal('test'), string, number, boolean)
expect(check('test___123false')).toEqual({
tag: 'success',
success: 'test___123false'
})
expect(check('test___false')).toEqual({
tag: 'failure',
failure: {
value: 'test___false',
errors: [{ issue: 'template literal mismatch', path: [] }]
}
})
Transformed code
|
tuple
Creates a tuple validator spec. Takes specs to validate tuple parts.
import { tuple, number, string, boolean } from 'spectypes'
const check = tuple(number, string, boolean)
expect(check([1, '2', false])).toEqual({
tag: 'success',
success: [1, '2', false]
})
expect(check([])).toEqual({
tag: 'failure',
failure: {
value: [],
errors: [{ issue: 'length is not 3', path: [] }]
}
})
expect(check([1, '2', false, 1000])).toEqual({
tag: 'failure',
failure: {
value: [1, '2', false, 1000],
errors: [{ issue: 'length is not 3', path: [] }]
}
})
expect(check(['1', '2', 'false'])).toEqual({
tag: 'failure',
failure: {
value: ['1', '2', 'false'],
errors: [
{ issue: 'not a number', path: [0] },
{ issue: 'not a boolean', path: [2] }
]
}
})
Transformed code
|
union
Creates a union validator spec. Takes specs to validate union cases.
import { union, number, string, boolean } from 'spectypes'
const check = union(number, string, boolean)
expect(check('temp')).toEqual({
tag: 'success',
success: 'temp'
})
expect(check(true)).toEqual({
tag: 'success',
success: true
})
expect(check(null)).toEqual({
tag: 'failure',
failure: {
value: null,
errors: [
{ issue: 'union case #0 mismatch: not a number', path: [] },
{ issue: 'union case #1 mismatch: not a string', path: [] },
{ issue: 'union case #2 mismatch: not a boolean', path: [] }
]
}
})
Transformed code
|
Utilities
transformer
Spec that tells babel plugin to generate a wrapper for an external transformer spec. Any spec containing struct, nullish, map, filter and transformer specs will create and return new object on successful validation. Such spec has to be wrapped with transformer when used inside another spec.
import { array, transformer, map, number } from 'spectypes'
const negated = map(number, (x) => -x)
const check = array(transformer(negated))
// Incorrect usage !!!
// const negated = transformer(map(number, (x) => -x))
// const check = array(negated)
expect(check([1, 2, -3])).toEqual({
tag: 'success',
success: [-1, -2, 3]
})
expect(check([1, 2, 'abc'])).toEqual({
tag: 'failure',
failure: {
value: [1, 2, 'abc'],
errors: [{ issue: 'not a number', path: [2] }]
}
})
Transformed code
|
validator
Spec that tells babel plugin to generate a wrapper for an external validator spec. Any spec not containing struct, nullish, map, filter and transformer specs on successful validation will return validated object. Such spec has to be wrapped with validator when used inside another spec.
import { array, validator, limit, number } from 'spectypes'
const positive = limit(number, (x) => x >= 0)
const check = array(validator(positive))
// Incorrect usage !!!
// const positive = validator(limit(number, (x) => x >= 0))
// const check = array(positive)
expect(check([0, 1, 2])).toEqual({
tag: 'success',
success: [0, 1, 2]
})
expect(check([-1, -2, -3])).toEqual({
tag: 'failure',
failure: {
value: [-1, -2, -3],
errors: [
{ issue: 'does not fit the limit', path: [0] },
{ issue: 'does not fit the limit', path: [1] },
{ issue: 'does not fit the limit', path: [2] }
]
}
})
Transformed code
|
lazy
Creates a spec to validate a value with recursive type. But data that recursively references itself is not supported. LazyTransformerSpec type should be used when spec contains struct, nullish,
map, filter and transformer specs, and LazyValidatorSpec otherwise.
import { lazy, string, object, array, validator, LazyValidatorSpec } from 'spectypes'
type Person = {
readonly name: string
readonly likes: readonly Person[]
}
const person: LazyValidatorSpec<Person> = lazy(() =>
object({ name: string, likes: array(validator(person)) })
)
expect(person({ name: 'Bob', likes: [{ name: 'Alice', likes: [] }] })).toEqual({
tag: 'success',
{ name: 'Bob', likes: [{ name: 'Alice', likes: [] }] }
})
expect(person({ name: 'Alice', likes: [{ name: 'Bob', likes: 'cats' }] })).toEqual({
tag: 'failure',
failure: {
value: { name: 'Alice', likes: [{ name: 'Bob', likes: 'cats' }] },
errors: [{ issue: 'not an array', path: ['likes', 0, 'likes'] }]
}
})
Transformed code
|
writable
Creates an empty validator that removes readonly modifiers from the result of validation
import { object, number, string, boolean, writable } from 'spectypes'
const check = writable(object({ x: number, y: string, z: boolean }))
expect(check({ x: 1, y: '2', z: true })).toEqual({
tag: 'success',
success: { x: 1, y: '2', z: true } // { x: number, y: string, z: true }
})
Spectype
Type to infer success value
import { object, number, string, boolean, Spectype } from 'spectypes'
const check = object({ x: number, y: string, z: boolean })
// { readonly x: number; readonly y: string; readonly z: boolean }
type Value = Spectype<typeof check>
Misc
Special cases
- When
literal(undefined)orunknownis used as a property validator insideobjectorstructand that property is not present in the validated object the validation will fail. - When
nullishis used as a property validator insideobjectorstructand that property is not present in the validated object the result will still contain that property set toundefined.
import { struct, nullish, literal, unknown } from 'spectypes'
const check = struct({ nullish, unknown, literal: literal(undefined) })
expect(check({ unknown: 1, literal: undefined })).toEqual({
tag: 'success',
success: { nullish: undefined, unknown: 1, literal: undefined }
})
expect(check({ literal: undefined })).toEqual({
tag: 'failure',
failure: {
value: { literal: undefined },
errors: [{ issue: 'missing key - unknown', path: [] }]
}
})
expect(check({ unknown: undefined })).toEqual({
tag: 'failure',
failure: {
value: { unknown: undefined },
errors: [{ issue: 'missing key - literal', path: [] }]
}
})
Transformed code
|
Result handling
Validators return their results as 'success or failure' wrapped values and does not throw any exceptions (other than those thrown by the functions passed to map, limit or filter). This library does not include any functions to process validation results, but a compatible handy package exists - ts-railway
Custom validators
There is no specific APIs to create custom validators, usually just unknown, map and limit are enough to create a validator for arbitrary data. For example, lets create a validator that checks if some value is a representation of a date and converts that value to Date object:
import { unknown, map, limit } from 'spectypes'
const check = map(
limit(unknown, (x) => !isNaN(Date.parse(x))),
(x) => new Date(x)
)
const date = new Date('Sun Apr 24 2022 12:51:57')
expect(check('Sun Apr 24 2022 12:51:57')).toEqual({
tag: 'success',
success: date
})
expect(check([1, 2, 'abc'])).toEqual({
tag: 'failure',
failure: {
value: [1, 2, 'abc'],
errors: [{ issue: 'does not fit the limit', path: [] }]
}
})
Transformed code
|
How is it tested?
Having 100% of the code covered with tests reflects only the coverage of generative code, not the generated one. It says little about the amount of potential bugs in this package. Because of that most of the test cases are randomly generated. When testing valid data validation it will generate spectypes validator and corresponding fast-check arbitrary, then validator will ensure that values provided by arbitrary are valid. When testing invalid data validation it will also generate an expected error, then validator will ensure that values provided by arbitrary are invalid and lead to expected error.