npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ok-computer

v2.0.2

Published

'Functions all the way down' data validation for JavaScript and TypeScript.

Downloads

922

Readme

OK Computer

GitHub package.json version GitHub license GitHub license deno land

λ "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.

Alt Text

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>;
// {
//   firstName: string;
//   lastName?: string | null | undefined;
//   picture: {
//     url: string;
//     width: number;
//   };
// };

const value: unknown = { lastName: 44, picture: {} };

const errors = user(value);
// {
//   firstName: 'Expected typeof string',
//   lastName: '(Expected nullish or expected typeof string)',
//   picture: {
//     url: '(Expected typeof string and expected length between 1 and 255)',
//     width: 'Expected integer'
//   }
// };

hasError(errors); // true

assert(value, user); // throw new AssertError('Invalid: first of 4 errors: firstName: Expected typeof string')
typeof value; // User

✨ 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:

  1. Returns undefined if the value is valid

  2. Returns an error (anything other than undefined) if the value is invalid

  3. 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); // undefined
fortyFour(43); // 'Expected the number 44'
fortyFour(Symbol.for('ok-computer.introspect')); // 'Expected the number 44'

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'); // undefined
string(10); // 'Expected string'
string(Symbol.for('ok-computer.introspect')); // 'Expected string'

The above validators implicitly handle rule 3 due to the nature of the validation logic. In some cases you need to explicitly handle it.

// 🚨 Bad
const symbol: Validator<symbol, string> = (value) =>
  typeof value !== 'symbol' ? 'Expected symbol' : undefined;

symbol(Symbol.for('cat')); // undefined ✅
symbol('cat'); // 'Expected symbol' ✅
symbol(Symbol.for('ok-computer.introspect')); // undefined ❌
// 👌 Better
const symbol: Validator<symbol, string> = (value) =>
  typeof value !== 'symbol' || value === Symbol.for('ok-computer.introspect')
    ? 'Expected symbol'
    : undefined;

symbol(Symbol.for('cat')); // undefined ✅
symbol('cat'); // 'Expected symbol' ✅
symbol(Symbol.for('ok-computer.introspect')); // 'Expected symbol' ✅
// 👍 Best
import { create } from 'ok-computer';

const symbol = create<symbol>((value) => typeof value === 'symbol')(
  'Expected symbol'
);

symbol(Symbol.for('cat')); // undefined ✅
symbol('cat'); // 'Expected symbol' ✅
symbol(Symbol.for('ok-computer.introspect')); // 'Expected symbol' ✅

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'); // undefined
string(44); // new Error('Expected string')

const number = create<number>((value) => typeof value === 'number')(false);
number(44); // undefined
number('cat'); // false

const never = create<never>((value) => false)(0);
never('cat'); // 0
never(44); // 0

const always = create((value) => true)({ id: 'foo.bar' });
always('cat'); // undefined
always(44); // undefined
always(Symbol.for('ok-computer.introspect')); // { id: 'foo.bar' }

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'); // undefined
length3([1, 2, 3]); // undefined
length3('catamaran'); // 'Expected length 3'
length3([1, 2]); // 'Expected length 3'
import { length, string, and } from 'ok-computer';

const name = and(string, length(3));

name('cat'); // undefined
name([1, 2, 3]); // (Expected typeof string and expected length 3)
name('catamaran'); // (Expected typeof string and expected length 3)
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'); // undefined
username(null); // undefined
username('cat'); // (Expected nullish or (Expected typeof string and expected length between 4 and 30 and expected to match pattern /^[\\w\\.]*$/ and not("Expected one of lewis.hamilton, kanye.west")))
username('lewis.hamilton'); // (Expected nullish or (Expected typeof string and expected length between 4 and 30 and expected to match pattern /^[\\w\\.]*$/ and not("Expected one of lewis.hamilton, kanye.west")))

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'); // undefined
jpeg('cat.png'); // 'Expected string to end with ".jpeg"'

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' }); // { name: undefined, [Symbol('ok-computer.structure')]: true }
user({ name: 44 }); // { name: 'Expected typeof string', [Symbol('ok-computer.structure')]: true }
import { array, string } from 'ok-computer';

const names = array(string);

names(['Hamilton']); // Array { 0: undefined, [Symbol('ok-computer.structure')]: true }
names(['Hamilton', 44]); // Array { 0: undefined, 1: 'Expected typeof string', [Symbol('ok-computer.structure')]: true }

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' }); // { name: undefined, [Symbol('ok-computer.structure')]: true }
isError(error); // false

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); // { firstName: 'Expected typeof string', lastName: 'Expected typeof string', [Symbol('ok-computer.structure')]: true }

isError(error); // true

hasError(error); // true (alias for `isError`)

listErrors(error); // [{ path: 'firstName', err: 'Expected typeof string' }, { path: 'lastName', err: 'Expected typeof string' }]

if (okay(value, user)) {
  typeof value; // { firstName: string; lastName: string }
}

assert(value, user); // throw new AssertError(`Invalid: first of 2 errors: firstName: Expected typeof string`)
typeof value; // { firstName: string; lastName: string }

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); // 'Expected typeof string'

const str = err(string, 'No really, I expected a string');
str(10); // 'No really, I expected a string'
import { err, nullish, string, or } from 'ok-computer';

const firstName = or(nullish, string);
firstName(10); // ORError(['Expected nullish', 'Expected typeof string'])

const forename = err(or(nullish, string), 'Expected nullish or string');
forename(10); // 'Expected nullish or string'

const vorname = err(or(nullish, string), 'Null oder Zeichenfolge erwartet');
vorname(10); // 'Null oder Zeichenfolge erwartet'

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); // ORError(['Expected nullish', 'Expected typeof string'])
JSON.stringify(err); // '(Expected nullish or expected typeof string)'
import { nullish, string, or, and, minLength } from 'ok-computer';

const firstName = or(nullish, and(string, minLength(1)));
const err = firstName(44); // ORError(['Expected nullish', ANDError(['Expected typeof string', 'Expected min length 1'])])

JSON.stringify(err); // '(Expected nullish or (Expected typeof string and expected min length 1))'
import { nullish, string, or, object } from 'ok-computer';

const firstName = or(nullish, object({ name: string }));
const err = firstName(44);
// ORError([
//   'Expected nullish',
//   {
//     name: 'Expected typeof string',
//     [Symbol('ok-computer.object-root')]: 'Expected object',
//     [Symbol('ok-computer.structure')]: true
//   }
// ])

// NOTE: Cannot be serialized into string
JSON.stringify(err); // { type: 'ORError', operator: 'OR', errors: ['Expected nullish', { name: 'Expected typeof string' }] }

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>; // string

const value: unknown = 'foo';

if (okay(value, validator)) {
  typeof value; // string
}

assert(value, validator); // throw new AssertError('Invalid: first of 1 error: (Expected typeof string and expected length 3)')
typeof value; // string
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>;
// { firstName: string; lastName?: string | null | undefined; }

const value: unknown = {};

if (okay(value, user)) {
  typeof value; // User
}

assert(value, user); // throw new AssertError('Invalid: first of 4 errors: firstName: (Expected typeof string and expected length 3)')
typeof value; // User

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>; // 44

const thirtyThree: Validator<33, string> = (value) =>
  value !== 33 ? 'Expected 33' : undefined;

type ThirtyThree = Infer<typeof thirtyThree>; // 33

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>; // 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

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:

  1. Discover the full API using TypeScript (TIP: import * as ok from 'ok-computer')
  2. Browse the source (there isn't much code)