OK Computer

λ "Functions all the way down" data validation for JavaScript and TypeScript.
🥞 Designed for frontend and backend.
💂 Advanced type inference, type guards and assertion functions.
🗣 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).
📦 Available on npm and deno.land. Runs anywhere.

Install | Example | Concepts | Type Inference | API Docs
Install
npm
npm install ok-computer
Yarn
yarn add ok-computer
Deno
import * as ok from "https://deno.land/x/ok_computer/ok-computer.ts";
Example
Try on CodeSandbox
import {
object,
string,
or,
nullish,
and,
length,
integer,
Infer,
hasError,
assert
} from 'ok-computer';
const user = object({
firstName: string,
lastName: or(nullish, string),
picture: object({
url: and(string, length(1, 255)),
width: integer
})
});
type User = Infer<typeof user>;
const value: unknown = { lastName: 44, picture: {} };
const errors = user(value);
hasError(errors);
assert(value, user);
typeof value;
✨ Concepts
Everything in OK Computer is a validation function, also known as a "validator".
type Validator<ValidType, Err = unknown> = (value: unknown) => Err | undefined;
A validator has 3 rules:
-
Returns undefined if the value is valid
-
Returns an error (anything other than undefined) if the value is invalid
-
Returns an error if the value is Symbol.for('ok-computer.introspect')
const fortyFour: Validator<number, string> = (value) =>
value !== 44 ? 'Expected the number 44' : undefined;
fortyFour(44);
fortyFour(43);
fortyFour(Symbol.for('ok-computer.introspect'));
All built-in validators work in this way, for example this is how string is implemented.
const string: Validator<string, string> = (value) =>
typeof value !== 'string' ? 'Expected string' : undefined;
string('cat');
string(10);
string(Symbol.for('ok-computer.introspect'));
The above validators implicitly handle rule 3 due to the nature of the validation logic. In some cases you need to explicitly handle it.
const symbol: Validator<symbol, string> = (value) =>
typeof value !== 'symbol' ? 'Expected symbol' : undefined;
symbol(Symbol.for('cat'));
symbol('cat');
symbol(Symbol.for('ok-computer.introspect'));
const symbol: Validator<symbol, string> = (value) =>
typeof value !== 'symbol' || value === Symbol.for('ok-computer.introspect')
? 'Expected symbol'
: undefined;
symbol(Symbol.for('cat'));
symbol('cat');
symbol(Symbol.for('ok-computer.introspect'));
import { create } from 'ok-computer';
const symbol = create<symbol>((value) => typeof value === 'symbol')(
'Expected symbol'
);
symbol(Symbol.for('cat'));
symbol('cat');
symbol(Symbol.for('ok-computer.introspect'));
NOTE: It's recommended to use create for all custom validators.
Errors don't have to be string values, as per rule 2 an error can be anything other than undefined. So yes, this means '', 0, null and false are all considered to be an error.
import { create } from 'ok-computer';
const string = create<string>((value) => typeof value === 'string')(
new Error('Expected string')
);
string('cat');
string(44);
const number = create<number>((value) => typeof value === 'number')(false);
number(44);
number('cat');
const never = create<never>((value) => false)(0);
never('cat');
never(44);
const always = create((value) => true)({ id: 'foo.bar' });
always('cat');
always(44);
always(Symbol.for('ok-computer.introspect'));
So far so good, however nothing particularly useful is going on as you don't need a library to write a function which conditionally returns undefined.
The real utility comes from higher order validators which accept arguments (in many cases arguments are themselves validators) and return new validators, allowing you to compose simple validators into more complex logic.
import { length } from 'ok-computer';
const length3 = length(3);
length3('cat');
length3([1, 2, 3]);
length3('catamaran');
length3([1, 2]);
import { length, string, and } from 'ok-computer';
const name = and(string, length(3));
name('cat');
name([1, 2, 3]);
name('catamaran');
import {
length,
string,
and,
or,
nullish,
pattern,
not,
oneOf
} from 'ok-computer';
const username = or(
nullish,
and(
string,
length(4, 30),
pattern(/^[\w\.]*$/),
not(oneOf('lewis.hamilton', 'kanye.west'))
)
);
username('catamaran');
username(null);
username('cat');
username('lewis.hamilton');
You can implement your own higher order validators in the same way.
import { create } from 'ok-computer';
const endsWith = (suffix: string) =>
create<string>(
(value) => typeof value === 'string' && value.endsWith(suffix)
)(`Expected string to end with "${suffix}"`);
const jpeg = endsWith('.jpeg');
jpeg('cat.jpeg');
jpeg('cat.png');
Some commonly used higher order validators return structural data which, like undefined, can also be considered valid.
import { object, string } from 'ok-computer';
const user = object({
name: string
});
user({ name: 'Hamilton' });
user({ name: 44 });
import { array, string } from 'ok-computer';
const names = array(string);
names(['Hamilton']);
names(['Hamilton', 44]);
This exposes a richer interface to consume more complex validation errors. The tradeoff being you can't simply check if the error is undefined to determine if it's valid. Instead you 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);
There are a number of other functions to help consume errors.
import {
object,
string,
isError,
hasError,
listErrors,
okay,
assert
} from 'ok-computer';
const user = object({
firstName: string,
lastName: string
});
const value: unknown = { firstName: 44 };
const error = user(value);
isError(error);
hasError(error);
listErrors(error);
if (okay(value, user)) {
typeof value;
}
assert(value, user);
typeof value;
Sometimes validation depends on sibling values. By convention all validators receive 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')
})
});
Although all out-the-box validators return pre-baked errors, you can override them with the err higher order validator.
import { err, string } from 'ok-computer';
string(10);
const str = err(string, 'No really, I expected a string');
str(10);
import { err, nullish, string, or } from 'ok-computer';
const firstName = or(nullish, string);
firstName(10);
const forename = err(or(nullish, string), 'Expected nullish or string');
forename(10);
const vorname = err(or(nullish, string), 'Null oder Zeichenfolge erwartet');
vorname(10);
Many errors returned from higher order validators such as ORError, ANDError, XORError, PeerError and NegateError serialize into string errors when possible.
import { nullish, string, or } from 'ok-computer';
const firstName = or(nullish, string);
const err = firstName(44);
JSON.stringify(err);
import { nullish, string, or, and, minLength } from 'ok-computer';
const firstName = or(nullish, and(string, minLength(1)));
const err = firstName(44);
JSON.stringify(err);
import { nullish, string, or, object } from 'ok-computer';
const firstName = or(nullish, object({ name: string }));
const err = firstName(44);
JSON.stringify(err);
Type Inference
OK Computer offers the ability to automatically infer a type from any given validator.
import { and, string, length, okay, assert } from 'ok-computer';
const validator = and(string, length(3));
type Type = Infer<typeof validator>;
const value: unknown = 'foo';
if (okay(value, validator)) {
typeof value;
}
assert(value, validator);
typeof value;
import { object, string, or, nullish, Infer, okay, assert } from 'ok-computer';
const user = object({
firstName: string,
lastName: or(nullish, string)
});
type User = Infer<typeof user>;
const value: unknown = {};
if (okay(value, user)) {
typeof value;
}
assert(value, user);
typeof value;
When creating your own validators, you can define an appropriate valid type up front.
import { create, Infer } from 'ok-computer';
const fortyFour = create<44>((value) => value === 44)('Expected 44');
type FortyFour = Infer<typeof fortyFour>;
const thirtyThree: Validator<33, string> = (value) =>
value !== 33 ? 'Expected 33' : undefined;
type ThirtyThree = Infer<typeof thirtyThree>;
However you can override the valid type on any validator using annotate.
import { annotate, and, integer, min, max } from 'ok-computer';
const num = annotate<1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10>()(
and(integer, min(1), max(10))
);
type Num = Infer<typeof num>;
NOTE: The double fn signature is required to ensure the error type can continue to be inferred. https://medium.com/@nandin-borjigin/partial-type-argument-inference-in-typescript-and-workarounds-for-it-d7c772788b2e
Type-first validator
In cases where you already have a type or interface defined, you can ensure the validator adheres to the type.
import { object, string, or, undef, Validator, ExtractErr } from 'ok-computer';
interface User {
readonly firstName: string;
readonly lastName?: string;
}
const _user = object({
firstName: string,
lastName: or(undef, string)
});
const user: Validator<User, ExtractErr<typeof _user>> = _user;
API
Coming soon... for now you can: