Hyperval
A simple Javascript data validation library
This library takes inspiration from superstruct.
It heavily borrows some of the grammar and coding patterns of the superstruct library.
-
Hyperval
- Why does this library exist ⇡
- Core design ⇡
- Installing ⇡
-
Example usage ⇡
- Smallest possible validation pattern ⇡
- Creating a reusable validation function ⇡
- Adding additional validation steps ⇡
- Mutate a value before validation ⇡
- Mutate a value after validation ⇡
- Assert a value ⇡
- Access raw error ⇡
- Objects and arrays ⇡
- Inferring types from the schema ⇡
- More complex object compositions ⇡
- Custom validation ⇡
- API documentation ⇡
- Contributing ⇡
⇡
Why does this library existI love superstruct, but as of v0.10.12 some of the core features are broken, or do not work as expected. For example, recursive coercion or intersection types. The superstruct library does not seem to be regularly updated anymore. After peeking around the superstruct source code, it didn't look too complicated to reimplement so I decided to make my own.
⇡
Core designThe validation system is based around the Hyper<In, Out>
class. Each function in this library creates a new Hyper
object.
The Hyper
object specifies the type of data allowed In
and type of data coming Out
.
The reason for specifying the Out
type is that in case of cast
and alter
the Out
will be different from In
.
As the modifiers are chained, the In
and Out
types will change, but the resulting end Hyper
will always contain initial allowed In
type
and return the final Out
type, masking all internal conversions.
Doing things this way allows us to easily wrap pre and post validation mutations inside the schema, as well as easily generate typescript types from it.
Extending the available types is also a simple matter of just creating a Hyper
object for primitives,
or a function that takes a Hyper
and returns another Hyper
for chained types.
The design philosophy of hyperval leans heavily on its inspiration superstruct, so you will see heavy similarities between the two. In fact, it is designed to be easy to migrate from superstruct to hyperval with a find and replace.
⇡
Design goals-
Typescript First: This is designed to be tightly integrated with typescript. A majority of the effort put in to making this library centers around creating types that just work.
-
Composable Interfaces: The schema can be easily broken down and composed, allowing you to extract common patterns in your schema and build bigger schemas with those patterns. Each
Hyper
object also recursively contains references to the source schema, so you can always extract nested schemas. -
Customizable Types: Augment the provided primitive types or define your own, or use the custom type helper function. Because validation design is simple, extending the available validation types is simple too.
-
Intuitive API: The API is design to look a bit like typescript types. hyperval fits right beside your typescript schema.
-
Mutate During Checking: Sometimes your data is coming from an unreliable source or needs to be altered before sending off elsewhere. Sometimes you want these mutations defined as part of your schema contract. You can do it with hyperval.
-
Don't Repeat Yourself: Define your schema once. Then derive your types through inference and your validators from the same place.
⇡
InstallingInstall the package hyperval
from npm.
yarn add hyperval
npm install hyperval
⇡
Example usageTo validate data, you need to import the data type and validation functions.
Here are various ways to use the library from simple to complex to get you started.
⇡
Smallest possible validation patternimport { string, is } from 'hyperval'
is(string(), '') === true
⇡
Creating a reusable validation functionexport const is_string = string().is
// somewhere else
import { is_string } from './validators'
is_string('') === true
⇡
Adding additional validation stepsimport { string, augment } from 'hyperval'
const is_password = augment(string(), x => x.length >= 8 && x.length <= 20).is
is_password('1234567') === false
is_password('12345678') === true
is_password('12345679012345678901') === false
⇡
Mutate a value before validationimport { number, cast } from 'hyperval'
const cast_number = cast(number(), x => +x).apply
cast_number(12345) === 12345
cast_number('12345') === 12345
⇡
Mutate a value after validationimport { number, alter } from 'hyperval'
const alter_number = alter(number(), x => x + '').apply
alter_number(12345) === '12345'
⇡
Assert a valueimport { string } from 'hyperval'
const x: string | number = ''
string().assert(x)
const y: string = x
⇡
Access raw errorimport { string } from 'hyperval'
/* HypervalError is the error object we use internally */
string().validate(0) === { error: HypevalError }
string().validate('') === { result: '' }
⇡
Objects and arraysimport { struct, array, string, number } from 'hyperval'
const is_object = struct({
id: string(),
autonumber: number(),
other_ids: array(string()),
deep: struct({
nested: array(
struct({
properties: string()
})
)
})
}).is
is_object({
id: '1',
autonumber: 1,
other_ids: ['2', '3', '4'],
deep: {
nested: [
{
properties: '5'
},
{
properties: '6'
},
{
properties: '7'
}
]
}
}) === true
⇡
Inferring types from the schemaimport type { Hyperval, Hyperout } from 'hyperval'
import { struct, array, string, number } from 'hyperval'
const object_schema = struct({
id: string(),
autonumber: number(),
other_ids: array(string()),
deep: struct({
nested: array(
struct({
properties: string()
})
)
})
})
/* The schema data before any mutations */
type Schema = Hyperval<typeof object_schema>
/* The schema data after any mutations */
type Schema = Hyperout<typeof object_schema>
⇡
More complex object compositionsimport { struct, object, record, map, enums, literal, optional, date, nullable, string, number, boolean, or, xor, and } from 'hyperval'
const is_people_tracker = struct({
people: object({
john: struct({
last: string()
}),
william: object({
last: string()
})
}),
track: enums([
struct({
person: literal('john'),
}),
object({
person: literal('william'),
date: optional(date())
})
]),
updated: nullable(number()),
owner: xor(
literal('tim'),
literal('dave')
),
owners: or(
literal('tim'),
literal('timmy')
),
all_owners: and(
object({ tim: boolean() }),
object({ dave: record(string(), string()) }),
object({ timmy: map(enums(1, literal(2)), string()) }),
)
}).is
is_people_tracker({
people: {
john: {
last: 'smith'
},
william: {
last: 'teller'
}
},
track: [
{
person: 'john'
},
{
person: 'william',
}
],
updated: null,
owner: 'tim',
owners: 'timmy',
all_owners: {
tim: true,
dave: {
last: 'dude',
nickname: 'davy'
},
timmy: new Map([[1, 'tim'], [2, 'temme']])
}
}) === true
is_people_tracker({
people: {
john: {
last: 'smith'
},
william: {
first: 'holy',
last: 'teller'
},
smith: {
last: 'agent'
}
},
track: [
{
person: 'john'
},
{
person: 'william',
date: new Date(),
time: Date.now()
}
],
updated: null,
owner: 'dave',
owners: 'tim',
all_owners: {
tim: false,
dave: {},
timmy: new Map()
}) === true
⇡
Custom validationimport { custom } from 'hyperval'
const is_signed_number = custom(
(x: any): { number: number, sign: '+' | '-' } => {
if (typeof x !== 'object' || x === null) return { error: 'Signed number must be object' }
if (Object.keys(x).length > 2 || !('number' in x) || !('sign' in x)) return { error: 'Signed number has incorrect properties' }
if (typeof x.number !== 'number' || !Math.isFinite(x.number)) return { error: 'Number is not a number' }
if (x.sign !== '+' && x.sign !== '-') return { error: 'Sign is incorrect' }
if ((x.number >= 0) !== (x.sign === '+')) return { error: 'Sign is inverted' }
return x
}
).is
is_signed_number({ number: 10, sign: '+' }) === true
is_signed_number({ number: -1, sign: '-' }) === false
⇡
API documentation⇡
Hyperinterface Hyper<In, Out> {
/* The validation function used internally. */
hyper.hyper
/* This value will appear on modifier, composite and composer generated `Hyper` containing the schema used to generate it */
hyper.schema
/* The message printed on validation failure. */
hyper.onfail
/* Validates and applies mutations to a value. Returning mutated value. */
hyper.apply(data) // Accepts any input data.
hyper.apply(data, 'strict') // This function will only allow data exactly matching the schema. 'strict' does nothing, it's just a inteliisense trick
/* Asserts a value to be of input type. */
hyper.assert(data) // Accepts any input data.
hyper.assert(data, 'strict') // This function will only allow data exactly matching the schema. 'strict' does nothing, it's just a inteliisense trick
/* Type guards a value to be of input type. */
hyper.is(data) // Accepts any input data.
hyper.is(data, 'strict') // This function will only allow data exactly matching the schema. 'strict' does nothing, it's just a inteliisense trick
/* Validates and mutates a value and returns the intermediate validation representation. */
hyper.validate(data) // Accepts any input data.
hyper.validate(data, 'strict') // This function will only allow data exactly matching the schema. 'strict' does nothing, it's just a inteliisense trick
/* Validates and applies mutations to a promised value. Returning mutated value. */
hyper.thenApply(data) // Accepts any input data.
hyper.thenApply(data, 'strict') // This function will only allow data exactly matching the schema. 'strict' does nothing, it's just a inteliisense trick
/* Asserts a promised value to be of input type. */
hyper.thenAssert(data) // Accepts any input data.
hyper.thenAssert(data, 'strict') // This function will only allow data exactly matching the schema. 'strict' does nothing, it's just a inteliisense trick
/* Type guards a promised value to be of input type. */
hyper.thenIs(data) // Accepts any input data.
hyper.thenIs(data, 'strict') // This function will only allow data exactly matching the schema. 'strict' does nothing, it's just a inteliisense trick
/* Validates and mutates a promised value and returns the intermediate validation representation. */
hyper.thenValidate(data) // Accepts any input data.
hyper.thenValidate(data, 'strict') // This function will only allow data exactly matching the schema. 'strict' does nothing, it's just a inteliisense trick
}
Hyper is the schema class used to wrap the validation functions. It is generated internally and it is unlikely that you will have to construct one yourself.
⇡
Type helpers⇡
HyperVal type extractortype HyperVal<Hyper<In, Out>> = In extends Out ? (Out extends In ? In : Out) : Out
This type helper will return input type if it's equal to output, otherwise it will return output type.
⇡
HyperRaw type extractortype HyperRaw<Hyper<In, any>> = In
This type helper will return the input type of a hyper.
⇡
PrimitivesPrimitives make up the basic types. Every validation recursion terminates at these functions.
⇡
anyconst x = any()
/* Valid */
x.assert('')
x.assert(0)
x.assert(true)
x.assert(null)
x.assert(undefined)
x.assert(() => { })
Creates a Hyper
that validates any value to be true.
⇡
unknownconst x = unknown()
/* Valid */
x.assert('')
x.assert(0)
x.assert(true)
x.assert(null)
x.assert(undefined)
x.assert(() => { })
Creates a Hyper
that validates unknown value to be true.
⇡
neverconst x = never()
/* Invalid */
x.assert()
Creates a Hyper
that rejects any value. This function will always throw if used to assert
or apply
.
⇡
literalconst x = literal('value')
/* Valid */
x.assert('value')
/* Invalid */
x.assert('values')
Creates a Hyper
that only allows the exact value provided to be true. Uses same-value-zero (===
) equality.
⇡
enumsconst x = enums('a', 0, boolean())
/* Valid */
x.assert('a')
x.assert(0)
x.assert(true)
x.assert(false)
/* Invalid */
x.assert('0')
x.assert('true')
x.assert('other')
Creates a Hyper
that is true if any of the values match. Uses same-value-zero (===
) equality for values and recursively executes Hyper
if provided with one.
Use or
if you are using this mainly for Hyper
⇡
booleanconst x = boolean()
/* Valid */
x.assert(true)
x.assert(false)
/* Invalid */
x.assert('true')
x.assert(0)
Creates a Hyper
that validates booleans to be true. Does not perform automatic type casting.
⇡
numberconst x = number()
/* Valid */
x.assert(Number.MIN_VALUE)
x.assert(Number.MAX_VALUE)
x.assert(Number.Infinity)
/* Invalid */
x.assert(NaN)
x.assert('0')
Creates a Hyper
that validates numbers to be valid. Does not perform automatic type casting. Infinite is valid, NaN is invalid.
⇡
stringconst x = string()
/* Valid */
x.assert('')
x.assert(' a string ')
/* Invalid */
x.assert(0)
x.assert(true)
Creates a Hyper
that validates strings to be valid. Does not perform automatic type casting. Zero length strings are valid.
⇡
instanceclass T {}
const x = instance(T)
/* Valid */
x.assert(new T())
/* Invalid */
x.assert({ })
Creates a Hyper
that validates an object to be constructed from the provided class/constructor. Uses instanceof
.
⇡
funcconst x = func()
/* Valid */
x.assert(() => { })
/* Invalid */
x.assert({ })
const p: any = (input: any) => +input
func<(a: string) => number>().assert(p)
const num: number = y('0')
Creates a Hyper
that validates functions to be generic function type. You can override the generic to specify the output function type.
⇡
dateconst x = date()
/* Valid */
x.assert(new Date())
/* Invalid */
x.assert(Date.now())
Creates a Hyper
that validates date objects to be valid.
⇡
customconst x = custom((val: string | number) => Number.isFinite(+val) ? { result: +val } : { error: 'Not a number' })
/* Valid */
x.apply('0') === 0
/* Invalid */
x.assert('not a number')
/* Throws 'a string' */
custom('a string', x => typeof x === 'string').assert(0)
Define a custom primitive Hyper
. If you provide the name argument, it will show up in errors.
⇡
ModifiersModifiers changes the behavior of Hyper
functions. They can add pre or post mutations, as well additional validations or make them nullable or optional.
⇡
optionalconst x = optional(number())
/* Valid */
x.assert(0)
x.assert(undefined)
/* Invalid */
x.assert(null)
x.assert('')
Creates a Hyper
that validates a possibly undefined value. Use literal(undefined)
if you want the actual undefined value.
All object based validators will strip optional root properties from the object when they are undefined. Literal undefined values are preserved. Any optional values as part of a composed or modified type (and, or, xor, cast, alter, augment etc.) will not be stripped unless optional wraps the entire composed type. Basically if optional
is not at the bottom of the call stack (last to be called), the stripping will not occur. Likewise, optional property type detection can only occur if property is a root key.
⇡
nullableconst x = nullable(number())
/* Valid */
x.assert(0)
x.assert(null)
/* Invalid */
x.assert(undefined)
x.assert('')
Creates a Hyper
that validates a possibly null value. Use literal(null)
if you want an actual null value.
⇡
augmentconst x = augment(number(), x => x >= 0)
/* Valid */
x.assert(0)
x.assert(Number.MAX_SAFE_INTEGER)
/* Invalid */
x.assert(-1)
x.asserrt('1')
Creates a Hyper
that adds an additional validation step. The validation is applied after the previous validation and thus the input value is typed.
⇡
alterconst x = alter(number(), x => `${x}`)
/* Valid */
x.apply(0) === '0'
/* Invalid */
x.assert('0')
Creates a Hyper
that mutates a value after validation.
⇡
castconst x = cast(number(), x => +x)
/* Valid */
x.apply('0') === 0
x.apply(0) === 0
/* Invalid */
x.assert('number')
Creates a Hyper
that mutates a value before validation.
⇡
CompositeComposite functions deal with any object data structures like objects and arrays.
⇡
structconst x = struct({
a: literal(true),
z: optional(literal(true)),
})
/* Valid */
x.assert({ a: true })
x.assert({ a: true, z: true })
/* Invalid */
x.assert({ a: false })
x.assert({ b: true })
x.assert({ a: true, b: undefined })
Creates a Hyper
object validator that does not allow any extra properties. Any property defined as optional
or undefined will be optional on the object.
⇡
objectconst x = struct({
a: literal(true),
z: optional(literal(true)),
})
/* Valid */
x.assert({ a: true })
x.assert({ a: true, z: true })
x.assert({ a: true, b: true })
/* Invalid */
x.assert({ a: false })
x.assert({ b: true })
Creates a Hyper
object validator that allow extra properties. This is similar to the way typescript
does things. Any property defined as optional
or undefined will be optional on the object.
⇡
recordconst x = record(
augment(string(), x => Number.isNaN(+x)),
number()
)
/* Valid */
x.assert({ a: 0 })
x.assert({ a: 0, b: 1 })
/* Invalid */
x.assert({ '0': 0, '1': 1 })
x.assert({ a: '0' })
Creates a Hyper
object validator that matches the key and value Hyper
.
⇡
mapconst x = map(
augment(string(), x => Number.isNaN(+x)),
number()
)
/* Valid */
x.assert(new Map([[ 'a', 0 ]]))
x.assert(new Map([[ 'a', 0 ], [ 'b', 1 ]]))
/* Invalid */
x.assert(new Map([[ '0', 0 ], [ '1', 1 ]]))
x.assert(new Map([[ 'a', '0' ]]))
Creates as Hyper
validator that matches an ES6 Map.
⇡
arrayconst x = array(number())
/* Valid */
x.assert([ ])
x.assert([ 1, 2, 3 ])
/* Invalid */
x.assert([ '1' ])
x.assert([ undefined ])
Creates a Hyper
array validator to match containing data.
⇡
tupleconst x = tuple([1, 2, literal('3')])
/* Valid */
x.assert([ 1, 2, '3' ])
/* Invalid */
x.assert([ '3', 2, 1 ])
x.assert([ 1, 2 ])
x.assert([ ])
x.assert([ 4, 5, 6 ])
Creates a Hyper
tuple validator.
⇡
setconst x = set(number())
/* Valid */
x.assert(new Set([ ]))
x.assert(new Set([ 1, 2, 3 ]))
/* Invalid */
x.assert(new Set([ '1' ]))
x.assert(new Set([ undefined ]))
Creates as Hyper
validator that matches an ES6 Set.
⇡
ComposersComposers allow you to combine schemas using boolean logic.
⇡
orconst x = or(string(), number())
/* Valid */
x.assert('')
x.assert(0)
/* Invalid */
x.assert(true)
Creates a Hyper
validator that is true as long as any one of the provided schemas is true.
⇡
andconst x = and(object({ a: 1 }), object({ b: 2 }))
/* Valid */
x.assert({ a: 1, b: 2 })
x.assert({ a: 1, b: 2, c: 3 })
/* Invalid */
x.assert({ a: 1 })
x.assert({ b: 2 })
x.assert({ })
Creates a Hyper
validator that is true when all of the provided schemas are true.
⇡
xorconst x = xor(object({ a: 1 }), object({ b: 2 }))
/* Valid */
x.assert({ a: 1 })
x.assert({ b: 2 })
x.assert({ a: 1, c: 3 })
/* Invalid */
x.assert({ a: 1, b: 2 })
x.assert({ })
Creates a Hyper
validator that is true when one, and only one of the provided schemas is true.
⇡
ContributingI use this library for my own projects so I may not respond to feature requests. However feel free to suggest features or bugs.
If you really want a change and can't get it fast enough, do what I did and make your own.
This library is MIT licensed.
Things I would really appreciate help on are:
- [ ] Benchmarking (I have no idea how fast or slow this library is)
- [ ] Performance optimization (I doubt this library is optimized on both the memory and execution time aspects)
- [ ] Better errors (errors are kinda meh right now)
- [ ] Better types (eny typescript wizards out there?)
- [ ] More tests (more coverage is always better. I don't trust my own code)
- [ ] Documentation (less confusion is always better)