README
Ok Computer
λ "Functions all the way down" data validation for JavaScript and TypeScript.
🥞 Designed for frontend and backend.
🗣 First class support for custom error messages / bring your own i18n.
🔌 Don't like something? Need extra functionality? Write a function.
☕ Zero dependencies (it's < 500 lines of code including types).
Install
npm install ok-computer
Example
import { object, string, or, nullish, and, length, integer, hasError, assert } from 'ok-computer';
const validator = object({
firstName: string,
lastName: or(nullish, string),
picture: object({
url: and(string, length(1, 255)),
width: integer
})
});
const errors = validator({ lastName: 44, picture: {} });
hasError(errors);
// true
assert(errors);
// throw new ValidationError('Invalid: first of 3 errors: firstName: Expected string')
✨ Concepts
Good news! There's no special API to write your validation logic, you just write a function which accepts a value
and returns an error if invalid:
const fortyFour = (value) =>
value !== 44 ? 'Expected the number 44' : undefined;
fortyFour(44);
// undefined
fortyFour(43);
// 'Expected the number 44'
This is how all built-in validation functions work, for example this is how string
is implemented:
const string = (value) =>
typeof value !== 'string' ? 'Expected string' : undefined;
string('cat');
// undefined
string(44);
// 'Expected string'
This signature can be a little distracting however and it can feel more natural to return a boolean
. create
allows you to do this:
import { create } from 'ok-computer';
const string = create((value) => typeof value === 'string')('Expected string');
string('cat');
// undefined
string(44);
// 'Expected string'
You may be thinking create
seems like an unnecessary abstraction, however decoupling your validation logic from the error itself turns out to be a good pattern; particularly for i18n:
import { create } from 'ok-computer';
const $string = create((value) => typeof value === 'string');
const string = $string('Erwartete Zeichenfolge');
string('cat');
// undefined
string(44);
// Erwartete Zeichenfolge
NOTE: By convention a function prefixed with
$
hasn't yet received its error.
Errors don't have to be string values, an error can be anything other than undefined
. So yes, this means ''
, 0
, null
and false
or even () => {}
are all considered to be an error:
import { create } from 'ok-computer';
const $string = create((value) => typeof value === 'string');
const string = $string(new Error('Expected string'));
string('cat');
// undefined
string(44);
// new Error('Expected string')
Therefore, most of the built-in validation functions expose two versions, one accepting a custom error and another which is pre-loaded with an error string:
import { string, $string, number, $number } from 'ok-computer';
string(44);
// 'Expected string'
number('cat');
// 'Expected number'
const str = $string({ id: 'str.invalid' });
const num = $number({ id: 'num.invalid' });
str(44);
// { id: 'str.invalid' }
num('cat');
// { id: 'num.invalid' }
Additionally, many of the built-in functions accept arguments to offer greater utility:
import { length, $length } from 'ok-conputer';
const between2And3 = length(2, 3);
between2And3('cat');
// undefined
between2And3('catamaran');
// Expected length between 2 and 3
const $tween2And3 = $length(2, 3);
const tween2And3 = $tween2And3('Invalid');
tween2And3('cat');
// undefined
tween2And3('catamaran');
// Invalid
You can implement your own validation functions in the same way:
import { create } from 'ok-computer';
const $endsWith = (suffix) =>
create((value) => typeof value === 'string' && value.endsWith(suffix));
const endsWith = (suffix) =>
$endsWith(`Expected string to end with "${suffix}"`);
const jpeg = endsWith('.jpeg');
jpeg('cat.jpeg');
// undefined
jpeg('cat.png');
// 'Expected string to end with ".jpeg"'
These can then be customised and composed with one another into more sophisticated validation logic:
import { create, or, and, length } from 'ok-computer';
const endsWith = (suffix: string) =>
create((value) => typeof value === 'string' && value.endsWith(suffix))(
`Expected string to end with "${suffix}"`
);
const jpeg = or(endsWith('.jpeg'), endsWith('.jpg'));
const image = and(jpeg, length(10, 15));
image('catamaran.jpg');
// undefined
image('catamaran.png');
// (Expected string to end with ".jpeg" or expected string to end with ".jpg")
image('cat.jpeg');
// Expected length between 10 and 15
image('cat.png');
// ((Expected string to end with ".jpeg" or expected string to end with ".jpg") and expected length between 10 and 15)
Some built-in validation functions return more exotic data structures which, like undefined
, are also not considered to be an error:
import { object, string } from 'ok-computer';
const user = object({
name: string
});
user({ name: 'Hamilton' });
// {}
user({ name: 44 });
// { name: 'Expected string' }
NOTE:
{}
returned byobject
is a special data type and a plain{}
is still considered an error.
This exposes a richer interface to consume more complex validation errors. The tradeoff being you can't just check if the value is undefined
to determine if there's an error and instead must use a dedicated isError
function:
import { object, string, isError } from 'ok-computer';
const user = object({
name: string
});
const error = user({ name: 'Hamilton' });
// {}
isError(error);
// false
NOTE:
hasError
is additionally exported, which is merely an alias forisError
.
Sometimes validation depends on other values. By convention all validation functions receive their parent values as subsequent arguments:
import { object, string, create } from 'ok-computer';
const user = object({
password: string,
repeatPassword: create((value, parent) => value === parent.password)(
'Expected to match password'
),
nested: object({
repeatPassword: create(
(value, parent, grandParent) => value === grandParent.password
)('Expected to match password')
})
});
Lastly, there are a number of functions to help consume errors:
import { object, string, isError, listErrors, assert } from 'ok-computer';
const user = object({
firstName: string,
lastName: string
});
const error = user({ firstName: 44 });
// { firstName: 'Expected string', lastName: 'Expected string' }
isError(error);
// true
listErrors(error);
// [{ path: 'firstName', err: 'Expected string' }, { path: 'lastName', err: 'Expected string' }]
assert(error);
// throw new ValidationError(`Invalid: first of 2 errors: firstName: Expected string`)
API
is
Performs a strict equality check with `===`
import { is } from 'ok-computer';
const is44 = is(44);
is44(44);
// undefined
is44(33);
// 'Expected 44'
import { $is } from 'ok-computer';
const is44 = $is(44)(new Error('Expected 44'));
is44(44);
// undefined
is44(33);
// new Error('Expected 44')
typeOf
Performs a `typeof` check
import { typeOf } from 'ok-computer';
const string = typeOf('string');
string('cat');
// undefined
string(44);
// 'Expected typeof string'
import { $typeOf } from 'ok-computer';
const string = $typeOf('string')(new Error('Expected typeof string'));
string('cat');
// undefined
string(44);
// new Error('Expected typeof string')
string
Performs a `typeof 'string'` check
import { string } from 'ok-computer';
string('cat');
// undefined
string(44);
// 'Expected string'
import { $string } from 'ok-computer';
const string = $string(new Error('Expected string'));
string('cat');
// undefined
string(44);
// new Error('Expected string')
TODO: Document full API