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)

    Keywords

    none

    Install

    npm i hyperval

    DownloadsWeekly Downloads

    2

    Version

    0.8.2

    License

    MIT

    Unpacked Size

    177 kB

    Total Files

    132

    Last publish

    Collaborators

    • synthetic-goop