spectypes
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-check
powered tests - precise types, accurately infers all types and provides readable compile-time error messages
- browser friendly, uses
babel
to compile validators, so no eval
or new Function
involved - 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 and babel-plugin-spectypes
, which parses and compiles validators into functions:
npm i spectypes
npm i babel-plugin-spectypes -D
-
Add babel-plugin-spectypes
to plugins section in your babel
config:
"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
const check = (value) => {
let err
if (typeof value !== 'boolean') {
;(err = err || []).push({
issue: 'not a boolean',
path: []
})
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
literal
Creates a literal validator spec. literal
can 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
const check = (value) => {
let err
if (value !== 'test') {
;(err = err || []).push({
issue: "not a '" + 'test' + "' string literal",
path: []
})
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
const check = (value) => {
let err, result
if (value !== null && value !== undefined) {
;(err = err || []).push({
issue: "not 'null' or 'undefined'",
path: []
})
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: result }
}
|
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
const check = (value) => {
let err
if (typeof value !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: []
})
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
const check = (value) => {
let err
if (typeof value !== 'string') {
;(err = err || []).push({
issue: 'not a string',
path: []
})
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
const check = (value) => {
let err
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
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 }
}
|
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
const _filter = (x) => x > 1
const check = (value) => {
let err, result
result = []
if (!Array.isArray(value)) {
;(err = err || []).push({
issue: 'not an array',
path: []
})
} else {
let filterindex = 0
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]
})
}
if (!err && _filter(value_index)) {
result[filterindex++] = value_index
}
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: result }
}
|
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']
})
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
const _limit = (x) => x > 1
const check = (value) => {
let err
if (typeof value !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: []
})
} else if (!_limit(value)) {
;(err = err || []).push({
issue: 'does not fit the limit',
path: []
})
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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']
})
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
const _map = (x) => x + 1
const check = (value) => {
let err, result
if (typeof value !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: []
})
} else {
result = _map(value)
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: result }
}
|
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
const check = (value) => {
let err
if (!Array.isArray(value)) {
;(err = err || []).push({
issue: 'not an array',
path: []
})
} else if (value.length < 2) {
;(err = err || []).push({
issue: 'length is less than ' + 2,
path: []
})
} else {
const value_$30_ = value[0]
if (typeof value_$30_ !== 'string') {
;(err = err || []).push({
issue: 'not a string',
path: [0]
})
}
const value_$31_ = value[1]
if (typeof value_$31_ !== 'string') {
;(err = err || []).push({
issue: 'not a string',
path: [1]
})
}
for (let index = 2; index < value.length; index++) {
const value_index = value[index]
if (typeof value_index !== 'boolean') {
;(err = err || []).push({
issue: 'not a boolean',
path: [index]
})
}
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
import * as _spectypes from 'spectypes'
const check = (value) => {
let err
if (!(typeof value === 'object' && value !== null && !Array.isArray(value))) {
;(err = err || []).push({
issue: 'not an object',
path: []
})
} else {
for (let i = 0; i < _spectypes.bannedKeys.length; i++) {
const ban = _spectypes.bannedKeys[i]
if (Object.prototype.hasOwnProperty.call(value, ban)) {
;(err = err || []).push({
issue: "includes banned '" + ban + "' key",
path: []
})
}
}
const value_x = value.x
if (typeof value_x !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: ['x']
})
}
for (const key in value) {
if (!(key === 'x')) {
const value_key = value[key]
if (typeof value_key !== 'boolean') {
;(err = err || []).push({
issue: 'not a boolean',
path: [key]
})
}
}
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
const check = (value) => {
let err
if (!(typeof value === 'object' && value !== null && !Array.isArray(value))) {
;(err = err || []).push({
issue: 'not an object',
path: []
})
} else {
const value_x = value.x
if (typeof value_x !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: ['x']
})
}
const value_y = value.y
if (typeof value_y !== 'string') {
;(err = err || []).push({
issue: 'not a string',
path: ['y']
})
}
const value_z = value.z
if (typeof value_z !== 'boolean') {
;(err = err || []).push({
issue: 'not a boolean',
path: ['z']
})
}
for (const key in value) {
if (!(key === 'x' || key === 'y' || key === 'z')) {
;(err = err || []).push({
issue: 'excess key - ' + key,
path: []
})
}
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
const check = (value) => {
let err, result
result = {}
if (!(typeof value === 'object' && value !== null && !Array.isArray(value))) {
;(err = err || []).push({
issue: 'not an object',
path: []
})
} else {
const value_x = value.x
if ('x' in value) {
if (value_x !== undefined) {
if (typeof value_x !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: ['x']
})
}
}
result.x = value_x
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: result }
}
|
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
import * as _spectypes from 'spectypes'
const check = (value) => {
let err
if (!(typeof value === 'object' && value !== null && !Array.isArray(value))) {
;(err = err || []).push({
issue: 'not an object',
path: []
})
} else {
for (let i = 0; i < _spectypes.bannedKeys.length; i++) {
const ban = _spectypes.bannedKeys[i]
if (Object.prototype.hasOwnProperty.call(value, ban)) {
;(err = err || []).push({
issue: "includes banned '" + ban + "' key",
path: []
})
}
}
for (const key in value) {
const value_key = value[key]
if (typeof value_key !== 'boolean') {
;(err = err || []).push({
issue: 'not a boolean',
path: [key]
})
}
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
const check = (value) => {
let err, result
result = {}
if (!(typeof value === 'object' && value !== null && !Array.isArray(value))) {
;(err = err || []).push({
issue: 'not an object',
path: []
})
} else {
const value_x = value.x
if (typeof value_x !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: ['x']
})
}
result.x = value_x
const value_y = value.y
if (typeof value_y !== 'string') {
;(err = err || []).push({
issue: 'not a string',
path: ['y']
})
}
result.y = value_y
const value_z = value.z
if (typeof value_z !== 'boolean') {
;(err = err || []).push({
issue: 'not a boolean',
path: ['z']
})
}
result.z = value_z
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: result }
}
|
template
Creates a template string validator spec. Takes number
, string
, boolean
, literal
specs and their union
s 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
import * as _spectypes from 'spectypes'
const _template = new RegExp(
'^' +
_spectypes.escapeRegexp('test') +
_spectypes.stringTest +
_spectypes.numberTest +
_spectypes.booleanTest +
'$'
)
const check = (value) => {
let err
if (typeof value !== 'string') {
;(err = err || []).push({
issue: 'not a string',
path: []
})
} else if (!_template.test(value)) {
;(err = err || []).push({
issue: 'template literal mismatch',
path: []
})
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
const check = (value) => {
let err
if (!Array.isArray(value)) {
;(err = err || []).push({
issue: 'not an array',
path: []
})
} else if (value.length !== 3) {
;(err = err || []).push({
issue: 'length is not ' + 3,
path: []
})
} else {
const value_$30_ = value[0]
if (typeof value_$30_ !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: [0]
})
}
const value_$31_ = value[1]
if (typeof value_$31_ !== 'string') {
;(err = err || []).push({
issue: 'not a string',
path: [1]
})
}
const value_$32_ = value[2]
if (typeof value_$32_ !== 'boolean') {
;(err = err || []).push({
issue: 'not a boolean',
path: [2]
})
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
const check = (value) => {
let err
if (!(typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean')) {
if (typeof value !== 'number') {
;(err = err || []).push({
issue: 'union case #0 mismatch: not a number',
path: []
})
}
if (typeof value !== 'string') {
;(err = err || []).push({
issue: 'union case #1 mismatch: not a string',
path: []
})
}
if (typeof value !== 'boolean') {
;(err = err || []).push({
issue: 'union case #2 mismatch: not a boolean',
path: []
})
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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))
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
const _map = (x) => -x
const negated = (value) => {
let err, result
if (typeof value !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: []
})
} else {
result = _map(value)
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: result }
}
const check = (value) => {
let err, result
result = []
if (!Array.isArray(value)) {
;(err = err || []).push({
issue: 'not an array',
path: []
})
} else {
for (let index = 0; index < value.length; index++) {
let result_index
const value_index = value[index]
const ext_value_index0 = negated(value_index)
if (ext_value_index0.tag === 'failure') {
;(err = err || []).push(
...ext_value_index0.failure.errors.map((fail) => ({
issue: '' + fail.issue,
path: [index, ...fail.path]
}))
)
} else {
result_index = ext_value_index0.success
}
result[index] = result_index
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: result }
}
|
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))
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
const _limit = (x) => x >= 0
const positive = (value) => {
let err
if (typeof value !== 'number') {
;(err = err || []).push({
issue: 'not a number',
path: []
})
} else if (!_limit(value)) {
;(err = err || []).push({
issue: 'does not fit the limit',
path: []
})
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
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]
const ext_value_index0 = positive(value_index)
if (ext_value_index0.tag === 'failure') {
;(err = err || []).push(
...ext_value_index0.failure.errors.map((fail) => ({
issue: '' + fail.issue,
path: [index, ...fail.path]
}))
)
}
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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
const person = (value) => {
let err
if (!(typeof value === 'object' && value !== null && !Array.isArray(value))) {
;(err = err || []).push({
issue: 'not an object',
path: []
})
} else {
const value_name = value.name
if (typeof value_name !== 'string') {
;(err = err || []).push({
issue: 'not a string',
path: ['name']
})
}
const value_likes = value.likes
if (!Array.isArray(value_likes)) {
;(err = err || []).push({
issue: 'not an array',
path: ['likes']
})
} else {
for (let index_likes = 0; index_likes < value_likes.length; index_likes++) {
const value_likes_index_likes = value_likes[index_likes]
const ext_value_likes_index_likes0 = person(value_likes_index_likes)
if (ext_value_likes_index_likes0.tag === 'failure') {
;(err = err || []).push(
...ext_value_likes_index_likes0.failure.errors.map((fail) => ({
issue: '' + fail.issue,
path: ['likes', index_likes, ...fail.path]
}))
)
}
}
}
for (const key in value) {
if (!(key === 'name' || key === 'likes')) {
;(err = err || []).push({
issue: 'excess key - ' + key,
path: []
})
}
}
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: value }
}
|
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 }
})
Spectype
Type to infer success
value
import { object, number, string, boolean, Spectype } from 'spectypes'
const check = object({ x: number, y: string, z: boolean })
type Value = Spectype<typeof check>
Misc
Special cases
- When
literal(undefined)
or unknown
is used as a property validator inside object
or struct
and that property is not present in the validated object the validation will fail. - When
nullish
is used as a property validator inside object
or struct
and that property is not present in the validated object the result will still contain that property set to undefined
.
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
const check = (value) => {
let err, result
result = {}
if (!(typeof value === 'object' && value !== null && !Array.isArray(value))) {
;(err = err || []).push({
issue: 'not an object',
path: []
})
} else {
let result_nullish
const value_nullish = value.nullish
if (value_nullish !== null && value_nullish !== undefined) {
;(err = err || []).push({
issue: "not 'null' or 'undefined'",
path: ['nullish']
})
}
result.nullish = result_nullish
const value_unknown = value.unknown
if (!('unknown' in value)) {
;(err = err || []).push({
issue: 'missing key - unknown',
path: []
})
}
result.unknown = value_unknown
const value_literal = value.literal
if (!('literal' in value)) {
;(err = err || []).push({
issue: 'missing key - literal',
path: []
})
}
if (value_literal !== undefined) {
;(err = err || []).push({
issue: "not a '" + undefined + "' literal",
path: ['literal']
})
}
result.literal = value_literal
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: result }
}
|
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
const _map = (x) => new Date(x)
const _limit = (x) => !isNaN(Date.parse(x))
const check = (value) => {
let err, result
if (!_limit(value)) {
;(err = err || []).push({
issue: 'does not fit the limit',
path: []
})
} else {
result = _map(value)
}
return err
? { tag: 'failure', failure: { value, errors: err } }
: { tag: 'success', success: result }
}
|
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.