SkillAgentSearch skills...

Bueno

Composable validators for forms, API:s in TypeScript

Install / Use

/learn @philipnilsson/Bueno
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<br /> <img src="./src/docs/logo.png" width="300px" /> <br />

A tiny, composable validation library. Bueno primary aims to be an improvement on form validation libraries like yup and superstruct, but can also be used as a lightweight API validation library. You'll like it if you need something

<p> 🌳 Small & <a href="https://webpack.js.org/guides/tree-shaking/">tree-shakeable</a>. </p> <p> 💡 Expressive! Use full boolean logic to compose your schemas </p> <p> 💫 Bidirectional. <a href="./src/docs/bidirectional.md">Learn more</a> </p> <p> 🚀 Awesome error messages in multiple languages supported out of the box, with more on the way. <a href="./src/docs/languages.md">Learn more</a> </p> <p> ⏱ Asynchronous (when needed!) </p>

Try it out

You can check out bueno directly in the browser in this jsfiddle.

Installation

Install using npm install --save bueno or yarn add bueno.

Check out the quickstart section below, or go directly to the <a href="#api-documentation">API docs</a>

<ul> <li><a href="./src/docs/form.md">Usage example: Vanilla JS form</a></li> <li><a href="./src/docs/express.md">Usage example: <code>express</code></a></li> <li><a href="./src/docs/formik.md">Usage example: <code>react</code> + <code>formik</code></a></li> <li><a href="./src/docs/customizing-errors.md">Customizing error messages</a></li> </ul>

Quickstart

bueno allows you to quickly and predictably compose validation schemas. Here's how it looks in action:

import { alphaNumeric, atLeast, check, checkPerKey, deDE, describePaths, either, email, enUS, length, moreThan, not, number, object, optional, string, svSE } from 'bueno'

const username = 
  string(length(atLeast(8)), alphaNumeric)

const age = 
  number(atLeast(18), not(moreThan(130)))

const user = object({
  id: either(email, username),
  age: optional(age)
})

const input = {
  id: 'philip@example.com', 
  age: 17 
}

console.log(check(input, user, enUS))
// 'Age must be at least 18 or left out'

console.log(check(input, user, describePaths(svSE, [['age', 'Ålder']])))
// 'Ålder måste vara som minst 18'

console.log(checkPerKey(input, user, deDE))
// { age: 'Muss mindestens 18 sein' }

Try this example in a Fiddle

API documentation

Core

Schemas are constructed using <a href="#basic-schemas">basic schemas</a> like number string, atLeast(10), exactly(null) and by using <a href="#combinator-api">combinators</a> like either, object, array, fix to create more complex schemas.

Most schemas (specifically Schema_:s) can be called as functions with other schemas as arguments. E.g.

number(even, atLeast(10))

The semantics are a schema returning the value of number with the additional validations from even and atLeast(10) taking place.

Running a schema

checkcheckPerKeyresult

<hr/>

The following functions allow you to feed input into a schema to parse & validate it. Note that schema evaluation is cached, so calling e.g. check(input) then immediately result(input) is not inefficient.

check

checkAsync :: <A>(value : A, schema : Schema<A, any>, locale : Locale) : string | null

Returns a string with a validation error constructed using the given locale, or null if validation succeeded.

check('123', number, enUS)
// 'Must be a number'

checkByKey

Returns an object of errors for each key in an object (for a schema constructed using the object combinator)

checkByKey({ n: '123', b: true }, object({ n: number, b: boolean }, enUS)
// { n: 'Must be a number', b: 'Must be a boolean' }

result

Returns the result of parsing using a schema.

result({ n: '123', d: 'null' }, object({ n: toNumber, d: toJSON })
// { n: 123, d: null }

checkAsync, checkByKeyAsync and resultAsync

The async versions of check, checkByKey and result respectively.

Combinator API

<a href="#apply">apply</a><a href="#both">both</a><a href="#compact">compact</a><a href="#defaultto">defaultTo</a><a href="#either">either</a><a href="#every">every</a><a href="#fix">fix</a><a href="#flip">flip</a><a href="#not">not</a><a href="#object">object</a><a href="#optional">optional</a><a href="#pipe">pipe</a><a href="#self">self</a><a href="#setmessage">setMessage</a><a href="#some">some</a><a href="#when">when</a>

Combinators create new, more complex schemas out of existing, simpler schemas.

both

Creates a schema that satisfies both of its arguments.

both :: <A, B, C, D>(v : Schema<A, C>, w : Schema<B, D>,) => Schema_<A & B, C & D>
const schema =
  both(even, atLeast(10))

check(schema, 11, enUS)
// 'Must be even.'

check(schema,  8, enUS)
// 'Must be at least 10.'

check(schema,  12, enUS)
// null

You may prefer using the <a href="#core">call signatures</a> of schemas over using this combinator.

either

Creates a schema that satisfies either of its arguments.

either :: <A, B, C, D>(v : Schema<A, C>, w : Schema<B, D>,) => Schema_<A & B, C & D>
const schema =
  either(even, atLeast(10))

check(schema, 11, enUS)
// null

check(schema,  8, enUS)
// null

check(schema,  9, enUS)
// 'Must be even or at least 10'

optional

Make a schema also match undefined.

optional :: <A>(v : Schema<A, B>) : Schema<A | undefined, B | undefined>
const schema = optional(number)

check(schema,  9, enUS)
// null

check(schema, undefined, enUS)
// null

check(schema, null, enUS)
// 'Must be a number or left out

not

not :: <A, B>(v : Schema<A, B>) => Schema_<A, B>

Negates a schema. Note that negation only affect the "validation" and not the "parsing" part of a schema. Essentially, remember that not does not affect the type signature of a schema.

For example, not(number) is the same as just number. The reason is that we can't really do much with a value that we know only to have type "not a number".

const schema = 
  number(not(moreThan(100)))

check(103, schema, enUS)
// Must not be more than 100

object

Create a schema on objects from an object of schemas.

object :: <AS, BS>(vs : 
  { [Key in keyof AS]: Schema<AS[Key], any> } & 
  { [Key in keyof BS]: Schema<any, BS[Key]> }
) => Schema_<AS, BS>
const schema = object({ 
  age: number, 
  name: string 
})

check({ age: 13 }, schema, enUS)
// Name must be a string

check({ age: '30', name: 'Philip' }, schema, enUS)
// Age must be a number

check({ age: 30, name: 'Philip' }, schema, enUS)
// null

You can use compact to make undefined keys optional.

inexactObject and exactObject are versions of this that are more lenient / strict w.r.t keys not mentioned in the schema.

compact

Remove keys in an object that are undefined.

compact :: <A, B>(p : Schema<A, B>) : Schema_<UndefinedOptional<A>, UndefinedOptional<B>> {
const schema =
  compact(object({ n: optional(number) }))

result({ n: undefined }, schema)
// {}

fix

Create a schema that can recursively be defined in terms itself. Useful for e.g. creating a schema that matches a binary tree or other recursive structures.

fix :: <A, B = A>(fn : (v : Schema<A, B>) => Schema<A, B>) => Schema_<A, B>

TypeScript is not too great at inferring types using this combinators, so typically help it using an annotation as below

type BinTree<A> = {
  left : BinTree<A> | null,
  right : BinTree<A> | null,
  value : A
}

const bintree = fix<BinTree<string>, BinTree<number>>(bintree => object({
  left: either(exactly(null), bintree),
  right: either(exactly(null), bintree),
  value: toNumber
}))

self

Create a schema dynamically defined in terms of its input.

type User = {
  verified : boolean,
  email : string | null
}

const schema = self<User, User>(user => {
  return object({
    verified: boolean,
    email: user.verified ? email : exactly(null)
  })
})

flip

Reverse a schema

flip :: <A, B>(schema : Schema<A, B>) => Schema_<B, A>
const schema = reverse(toNumber)

result(123, schema)
// '123'

defaultTo

Set a default value for a schema when it fails parsing.

defaultTo :: <A, B>(b: B, schema : Schema<A, B>) => Schema_<B, A>
const schema = 
  defaultTo(100, number)

result(null, schema)
// 100

pipe

Pipe the output of a schema as the input into another

pipe :: <A, B, C>(s : Schema<A, B>, t : Schema<B, C>) => Schema_<A, C>
const schema = 
   pipe(toNumber, lift(x => x + 1))

result('123', schema)
// 124

apply

Set the input of a schema to a fixed value. Can be used when creating a schema where the definition of one key depends on another.

apply :: <A>(v : Schema<A, A>, value : A, path : string) => Schema_<any, A>;
type Schedule = {
  weekday : string
  price : number
}

const schema = self((schedule : Schedule) => object({
  weekday: oneOf('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'),
  price: when(
    // When schedule.weekday is Sat or Sun, the
    // price must be at least 100, otherwise at most 50.
    apply(oneOf('Sat', 'Sun'), schedule.weekday, 'weekday'), 
    atLeast(100),
    atMost(50)
  )
})

every

A variable arguments version of both.

every :: <A, B>(...vs : Schema<A, B>[]) => Schema_<A, B>

setMessage

Set the error message of a parser.

const thing = setMessage(
  object({ foo: string, bar: number })
View on GitHub
GitHub Stars348
CategoryDevelopment
Updated1mo ago
Forks7

Languages

TypeScript

Security Score

80/100

Audited on Mar 7, 2026

No findings