hyperval
TypeScript icon, indicating that this package has built-in type declarations

0.8.2 • Public • Published

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.

Why does this library exist

I 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 design

The 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.

Installing

Install the package hyperval from npm.

yarn add hyperval
npm install hyperval

Example usage

To 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 pattern

import { string, is } from 'hyperval'

is(string(), '') === true

Creating a reusable validation function

export const is_string = string().is

// somewhere else
import { is_string } from './validators'

is_string('') === true

Adding additional validation steps

import { 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 validation

import { number, cast } from 'hyperval'

const cast_number = cast(number(), x => +x).apply

cast_number(12345) === 12345
cast_number('12345') === 12345

Mutate a value after validation

import { number, alter } from 'hyperval'

const alter_number = alter(number(), x => x + '').apply

alter_number(12345) === '12345'

Assert a value

import { string } from 'hyperval'

const x: string | number = ''

string().assert(x)

const y: string = x

Access raw error

import { string } from 'hyperval'

/* HypervalError is the error object we use internally */
string().validate(0) === { error: HypevalError }
string().validate('') === { result: '' }

Objects and arrays

import { 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 schema

import 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 compositions

import { 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 validation

import { 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

Hyper

interface 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 extractor

type 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 extractor

type HyperRaw<Hyper<In, any>> = In

This type helper will return the input type of a hyper.

Primitives

Primitives make up the basic types. Every validation recursion terminates at these functions.

any

const 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.

unknown

const 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.

never

const x = never()

/* Invalid */
x.assert()

Creates a Hyper that rejects any value. This function will always throw if used to assert or apply.

literal

const 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.

enums

const 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

boolean

const 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.

number

const 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.

string

const 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.

instance

class 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.

func

const 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.

date

const x = date()

/* Valid */
x.assert(new Date())

/* Invalid */
x.assert(Date.now())

Creates a Hyper that validates date objects to be valid.

custom

const 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.

Modifiers

Modifiers changes the behavior of Hyper functions. They can add pre or post mutations, as well additional validations or make them nullable or optional.

optional

const 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.

nullable

const 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.

augment

const 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.

alter

const x = alter(number(), x => `${x}`)

/* Valid */
x.apply(0) === '0'

/* Invalid */
x.assert('0')

Creates a Hyper that mutates a value after validation.

cast

const 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.

Composite

Composite functions deal with any object data structures like objects and arrays.

struct

const 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.

object

const 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.

record

const 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.

map

const 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.

array

const 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.

tuple

const 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.

set

const 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.

Composers

Composers allow you to combine schemas using boolean logic.

or

const 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.

and

const 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.

xor

const 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.

Contributing

I 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)

Dependents (0)

Package Sidebar

Install

npm i hyperval

Weekly Downloads

17

Version

0.8.2

License

MIT

Unpacked Size

177 kB

Total Files

132

Last publish

Collaborators

  • synthetic-goop