Blork! Mini runtime type checking in Javascript
A mini type checker for locking down the external edges of your code. Mainly for use in modules when you don"t know who'll be using the code. Minimal boilerplate code keeps your functions hyper readable and lets them be their beautiful minimal best selves (...or something?)
Blork is fully unit tested and 100% covered (if you're into that!).
Installation
npm install blork
Usage
args(): Check function arguments
The primary use case of Blork is validating function input arguments. The args()
function is provided for this purpose, and should be passed two arguments:
arguments
| The arguments object provided automatically to functions in Javascripttypes
| An array identifying the types for the arguments (list of types is available below)
import { args } from "blork";
export default function myFunc(definitelyString, optionalNumber)
{
args(arguments, ["string", "number?"]);
return "It passed!";
}
myFunc("abc", 123);
myFunc("abc");
myFunc(123);
myFunc("abc", "abc");
myFunc();
myFunc("abc", 123, true);
check(): Check individual values
The check()
function allows you to test individual values with more granularity. The check()
function is more versatile and allows more use cases than validating function input arguments.
check()
accepts three arguments:
value
The value to checktype
The type to check the value against (list of types is available below)prefix
An optional string name/prefix for the value, which is prepended to any error message thrown to help debugging
import { check } from "blork";
check("Sally", "string");
check("Sally", String);
check("Sally", "number");
check("Sally", Boolean);
check("Sally", "num", "name");
check(true, "str", "status");
Another common use for check()
is to validate an options object:
import { check } from "blork";
function myFunc(options)
{
check(options, { name: "string", required: "boolean", keepAlive: "number?" });
}
There are more complex types available: Appending ?
question mark to any type string makes it optional (which means it also allows undefined
). Prepending a !
exclaimation mark to any type string makes it inverted. Multiple types can be combined with |
and &
for OR and AND conditions.
check(undefined, "number");
check(undefined, "number?");
check(null, "number?");
check(123, "!str");
check(123, "!int");
check(1234, "num | str");
check(null, "num | str");
check("abc", "string & !falsy");
check("", "string & !falsy");
Checking objects and arrays
Blork can perform deep checks on objects and arrays to ensure the schema is correct. To do object or array checks pass literal arrays or literal objects to check()
or args()
:
check({ name: "Sally" }, { name: "string" });
check(["Sally", "John", "Sonia"], ["str"]);
check([1029, "Sonia"], ["number", "string"]);
check({ name: "Sally" }, { name: "string" });
check(["Sally", "John", "Sonia"], ["str"]);
check([1029, "Sonia"], ["number", "string"]);
check([1029, "Sonia", true], ["number", "string"]);
Arrays and objects can be deeply nested within each other and Blork will recursively check the schema all the way down:
check(
[
{ id: 1028, name: "Sally", status: [1, 2, 3] },
{ id: 1062, name: "Bobby", status: [1, 2, 3] }
],
[
{ id: Number, name: String, status: [Number] }
]
);
check(
[
{ id: 1028, name: "Sally", status: [1, 2, 3] },
{ id: 1062, name: "Bobby", status: [1, 2, "not_a_number"] }
],
[
{ id: Number, name: String, status: [Number] }
]
);
add(): Add a custom checker type
Register your own checker using the add()
function. This is great if 1) you're going to be applying the same check over and over, or 2) want to integrate your own checks with Blork's built-in types so your code looks clean.
add()
accepts four arguments:
name
The name of the custom checker (only kebab-case strings allowed).checker
A function that accepts a single argument, value
, and returns true
or false
.description=""
An description for the value the checker will accept, e.g. "lowercase string" or "unique username", that is shown in the error message. Defaults to the value of name
.error=undefined
A custom class that is thrown when this checker fails (can be [VALUES]_ class, not just classes extending Error
). An error set with add() takes precedence for this checker over the error set through
throws()`.
import { add, check } from "blork";
add(
"catty",
(v) => typeof v === "string" && v.strToLower().indexOf("cat") >= 0,
"string containing 'cat'"
);
check("That cat is having fun", "catty");
check("That CAT is having fun", "catty");
check("A dog sits on the chair", "catty");
check("A CAT SAT ON THE MAT", "upper+ & catty");
check("A DOG SAT ON THE MAT", "upper+ & catty");
import { add, args } from "blork";
function myFunc(str)
{
args(arguments, ["catty"]);
return "It passed!";
}
myFunc("That cat is chasing string");
myFunc("A dog sits over there");
throws(): Set a custom error constructor
To change the error object Blork throws when a type doesn't match, use the throws()
function. It accepts a single argument a custom class (can be [VALUES]_ class, not just classes extending Error
).
import { throws, check } from "blork";
class MyError extends Error {};
throws(MyError);
check(true, "false");
blork(): Create an independent instance of Blork
To create an instance of Blork with an independent set of checkers (added with add()
) and an independently set throws()
error object, use the blork()
function.
This functionality is provided so you can ensure multiple versions of Blork in submodules of the same project don't interfere with each other, even if they have been (possibly purposefully) deduped in npm. This is how you can ensure if you've set a custom error for a set of checks, that custom error type is always thrown.
import { blork } from "blork";
const { check, args, add, throws } = blork();
throws(class CustomError extends ValueError);
add("mychecker", v => v === "abc", "'abc'");
check("123", "mychecker");
props(): Define Blork-checked object properties
The props()
function can define an object properties (like Object.defineProperties()
) that are readable and writable, BUT the value must always match the type it was initially defined with.
This allows you to create objects with properties that have a guaranteed type. This makes your object more robust and removes the need to check the type of the property before using it.
props()
accepts two arguments:
object
The object to define the property onprops
A set of properties to define on the object and lock down
import { prop } from "blork";
const obj = {};
props(obj, {
"name": "Mel",
"coords": { lat: 0, lng: 0 },
"map": new Map()
});
obj.name = "John";
obj.coords = { lat: 28.20, lng: 12.00 };
obj.map = new Map();
obj.name = 123;
obj.coords = 123;
obj.coords = { lat: "abc", lng: 0 };
obj.map = new Set();
import { prop } from "blork";
const obj = {};
debug(): Debug any value as a string.
Blork exposes its debugger helper function debug()
, which it uses to format error messages correctly. debug()
accepts any argument and will return a clear string interpretation of the value.
debug()
deals well with large and nested objects/arrays by inserting linebreaks and tabs if line length would be unreasonable. Output is also kept cleanish by only debugging 3 levels deep, truncating long strings, and not recursing into circular references.
import debug from "blork";
debug(undefined);
debug(null);
debug(true);
debug(false);
debug(123);
debug("abc");
debug(Symbol("abc"));
debug(function dog() {});
debug(function() {});
debug({});
debug({ a: 123 });
debug(new Promise());
debug(new MyClass());
debug(new class {}());
ValueError: Great debuggable error class
Internally, when there's a problem with a value, Blork will throw a ValueError
. This value extends TypeError
and standardises error message formats, so errors are consistent and provide the detail a developer should need to debug the issue error quickly and easily.
It accepts three values:
message
The error message describing the issue with the value, e.g. "Must be string"
value
The actual value that was incorrect so a debugged version of this value can appear in the error message, e.g. (received 123)
prefix
A string prefix for the error that should identify the location the error occurred and the name of the value, e.g. "myFunction(): name"
import { ValueError } from "blork";
function myFunc(name) {
if (typeof name !== "string") throw new ValueError("Must be string", name, "myFunc(): name");
}
myFunc(123);
Type reference
This section lists all types that are available in Blork. A number of different formats can be used for types:
- String types (e.g.
"promise"
and "integer"
) - String modifiers that modify those string types (e.g.
"?"
and "!"
) - Constant and constructor shorthand types (e.g.
null
and String
) - Object and Array literal types (e.g.
{}
and []
)
String types: primitives
Type string | Description |
---|
primitive | Any primitive value (undefined, null, booleans, strings, finite numbers) |
null | Value is null |
undefined , undef , void | Value is undefined |
defined , def | Value is not undefined |
String types: booleans
Type string | Description |
---|
boolean , bool | Value is true or false |
true | Value is true |
false | Value is false |
truthy | Any truthy values (i.e. == true) |
falsy | Any falsy values (i.e. == false) |
String types: numbers
Type string | Description |
---|
zero | Value is 0 |
one | Value is 1 |
nan | Value is NaN |
number , num | Any numbers except NaN/Infinity (using Number.isFinite()) |
+number , +num , | Numbers more than or equal to zero |
-number , -num | Numbers less than or equal to zero |
integer , int | Integers (using Number.isInteger()) |
+integer , +int | Positive integers including zero |
-integer , -int | Negative integers including zero |
String types: strings
Type string | Description |
---|
string , str | Any strings (using typeof) |
lower | lowercase string (non-empty and alphanumeric only) |
upper | UPPERCASE strings (non-empty and alphanumeric only) |
camel | camelCase strings e.g. variable/function names (non-empty alphanumeric with lowercase first letter) |
pascal | PascalCase strings e.g. class names (non-empty alphanumeric with uppercase first letter) |
snake | snake_case strings (non-empty alphanumeric lowercase) |
screaming | SCREAMING_SNAKE_CASE strings e.g. environment vars (non-empty uppercase alphanumeric) |
kebab , slug | kebab-case strings e.g. URL slugs (non-empty alphanumeric lowercase) |
train | Train-Case strings e.g. HTTP-Headers (non-empty with uppercase first letters) |
String types: objects
Type string | Description |
---|
function , func | Functions (using instanceof Function) |
object , obj | Plain objects (using typeof && !null and constructor check) |
objectlike | Any object-like object (using typeof && !null) |
iterable | Objects with a Symbol.iterator method (that can be used with for..of loops) |
circular | Objects with one or more circular references (use !circular to disallow circular references) |
array , arr | Plain arrays (using instanceof Array and constructor check) |
arraylike , arguments , args | Array-like objects, e.g. arguments (any object with numeric .length property, not just arrays) |
map | Instances of Map |
weakmap | Instances of WeakMap |
set | Instances of Set |
weakset | Instances of WeakSet |
promise | Instances of Promise |
date | Instances of Date |
future | Instances of Date with a value in the future |
past | Instances of Date with a value in the past |
regex , regexp | Instances of RegExp (regular expressions) |
symbol | Value is Symbol (using typeof) |
String types: other
Type string | Description |
---|
any , mixed | Allow any value (transparently passes through with no error) |
json , jsonable | JSON-friendly values (null, true, false, finite numbers, strings, plain objects, plain arrays) |
check("abc", "str");
check("abc", "lower");
check(100, "integer");
check([1, 2, 3], "array+");
check(new Date(2180, 1, 1), "future");
check(new Map([[1, 1], [2, 2]]), "map+");
check(123, "str");
check({}, "object+");
check([], "array+");
String modifiers: Optional types
Any string type can be made optional by appending a ?
question mark to the type reference. This means the check will also accept undefined
in addition to the specified type.
check(undefined, "str?");
check(undefined, "lower?");
check(undefined, "int?");
check([undefined, undefined, 123], ["number?"]);
check(123, "str?");
check(null, "str?");
String modifiers: Non-empty types
Any type can be made non-empty by appending a +
plus sign to the type reference. This means the check will only pass if the value is non-empty and has length.
check("abc", "str+");
check([1], "arr+");
check({ a: 1 }, "obj+");
check(123, "str+");
check([], "arr+");
check({}, "obj+");
String modifiers: Inverted types
Any string type can be made optional by prepending a !
question mark to the type reference. This means the check will only pass if the inverse of its type is true.
check(undefined, "!str");
check("Abc", "!lower");
check(123.456, "!integer");
check([undefined, "abc", true, false], ["!number"]);
check(123, "!str");
check(true, "!bool");
check([undefined, "abc", true, 123], ["!number"]);
String modifiers: Combined types
You can use &
and |
to join string types together, to form AND and OR chains of allowed types. This allows you to compose together more complex types like number | string
or date | number | null
or string && custom-checker
|
is used to create an OR type, meaning any of the values is valid, e.g. number|string
or string | null
check(123, "str|num");
check("a", "str|num");
check(null, "str|num");
check(null, "str|num|bool|func|obj");
&
is used to create an AND type, meaning the value must pass all of the checks to be valid. This is primarily useful for custom checkers e.g. lower & username-unique
.
add("catty", v => v.toLowerCase().indexOf("cat") >= 0);
check("this cat is crazy!", "lower & catty");
check("THIS CAT IS CRAZY", "upper & catty");
check("THIS CAT IS CRAZY", "lower & catty");
check("THIS DOG IS CRAZY", "string & catty");
Note: &
has a higher precedence than |
, meaning a type like string & lower | upper
compiles to (lower | upper) & string
.
Note: All built in checkers like lower
or int+
already check the basic type of a value, so there's no need to use string & lower
or number & int+
. These will work but you'll be double checking.
Note: Spaces around the &
or |
are not required (but can be more readable).
Constructor and constant types
For convenience some constructors (e.g. String
) and constants (e.g. null
) can be used as types in args()
and check()
. The following built-in objects and constants are supported:
Type | Description |
---|
Boolean | Same as 'boolean' type |
String | Same as 'string' type |
Number | Same as 'number' type |
true | Same as 'true' type |
false | Same as 'false' type |
null | Same as 'null' type |
undefined | Same as 'undefined' type |
You can pass in any class name, and Blork will check the value using instanceof
and generate a corresponding error message if the type doesn't match.
Using Object
and Array
constructors will work also and will allow any object that is instanceof Object
or instanceof Array
. Note: this is not the same as e.g. the 'object'
and 'array'
string types, which only allow plain objects and arrays.
check(true, Boolean);
check("abc", String);
check(123, Number);
check(new Date, Date);
check(new MyClass, MyClass);
check(Promise.resolved(true), Promise);
check([true, true, false], [Boolean]);
check({ name: 123 }, { name: Number });
check("abc", Boolean);
check("abc", String);
check("abc", String, "myVar");
check(new MyClass, OtherClass);
check({ name: 123 }, { name: String });
check({ name: 123 }, { name: String }, "myObj");
Object literal type
To check the types of object properties, use a literal object as a type. You can also deeply nest these properties and the types will be checked recursively and will generate useful debuggable error messages.
Note: it is fine for objects to contain additional properties that don't have a type specified.
check({ name: "abc" }, { name: "str" });
check({ name: "abc" }, { name: "str?", age: "num?" });
check({ name: "abc", additional: true }, { name: "str" });
check({ age: "apple" }, { age: "num" });
check({ size: { height: 10, width: "abc" } }, { size: { height: "num", width: "num" } });
Object literal type: additional values
To check that the type of any properties conform to a single type, use the VALUES
symbol and create a [VALUES]
key. This allows you to check objects that don't have known keys (e.g. from user generated data). This is similar to how indexer keys work in Flow or Typescript.
import { check, VALUES } from "blork";
check(
{ a: 1, b: 2, c: 3 },
{ [VALUES]: "num" }
);
check(
{ name: "Dan", a: 1, b: 2, c: 3 },
{ name: "str", [VALUES]: "num" }
);
check(
{ a: 1, b: 2, c: "abc" },
{ [VALUES]: "num" }
);
check(
{ name: "Dan", a: 1, b: 2, c: 3 },
{ name: "str", [VALUES]: "bool" }
);
You can use this functionality with the undefined
type to ensure objects do not contain additional properties (object literal types by default are allowed to contain additional properties).
check(
{ name: "Carl" },
{ name: "str", [VALUES]: "undefined" }
);
check(
{ name: "Jess", another: 28 },
{ name: "str", [VALUES]: "undefined" }
);
Object literal type: additional keys
To check that the keys of any additional properties conform to a single type, use the KEYS
symbol and create a [KEYS]
key. This allows you to ensure that keys conform to a specific string type, e.g. camelCase, kebab-case or UPPERCASE (see string types above).
import { check, VALUES } from "blork";
check({ MYVAL: 1 }, { [KEYS]: "upper" });
check({ myVal: 1 }, { [KEYS]: "camel" });
check({ MyVal: 1 }, { [KEYS]: "pascal" });
check({ my-val: 1 }, { [KEYS]: "kebab" });
check({ MYVAL: 1 }, { [KEYS]: "upper" });
check({ myVal: 1 }, { [KEYS]: "camel" });
check({ MyVal: 1 }, { [KEYS]: "pascal" });
check({ my-val: 1 }, { [KEYS]: "kebab" });
Object literal type: custom constructor
Normally object literal types check that the object is a plain object. If you wish to allow the object to be a different object, use the CLASS
symbol and create a [CLASS]
key.
import { check, CLASS } from "blork";
class MyClass {
constructor () {
this.num = 123;
}
}
check(
new MyClass,
{ num: 123, [CLASS]: MyClass }
);
check(
{ num: 123, },
{ num: 123, [CLASS]: MyClass }
);
Array literal type
To check an array where all items conform to a specific type, pass an array as the type. Arrays and objects can be deeply nested to check types recursively.
check(["abc", "abc"], ["str"]);
check([123, 123], ["num"]);
check([{ names: ["Alice", "John"] }], [{ names: ["str"] }]);
check(["abc", "abc", 123], ["str"]);
check(["abc", "abc", 123], ["number"]);
Array tuple type
Similarly, to check the format of tuples, pass an array with two or more items as the type. If two or more types are in an type array, it is considered a tuple type and will be rejected if it does not conform exactly to the tuple.
check([123, "abc"], ["num", "str"]);
check([123, "abc"], ["num", "str", "str?"]);
check([123], ["num", "str"]);
check([123, 123], ["num", "str"]);
check([123, "abc", true], ["num", "str"]);
Contributing
Please see (CONTRIBUTING.md)
Roadmap
Changelog
- 7.0.0
- Add
VALUES
, KEYS
, and CLASS
symbol constants - Remove
_any
key and use VALUES
to provide the same functionality - Add
KEYS
functionality to check type or case of object keys, e.g. camelCase or kebab-case - Add
CLASS
functionality to check the class of an object - Add string case checkers for e.g. variable names (kebab-case, camelCase, snake_case etc)
upper
and lower
checkers work differently (all characters must be UPPERCASE/lowercase)- Rename
int+
, int-
checkers to +int
and -int
- Add '+' modifier to check for non-empty values with any checker
- Remove hardcoded '+' checkers like
lower+
, object+
- Remove
uppercase
and lowercase
checkers for consistency
- 6.0.0
- Remove
prop()
function and add props()
function instead (prop()
was impossible to type with Flow)
- 5.1.0
- Add
prop()
function that defines a locked object property that must match a Blork type
- 5.0.0
- Change from symbol
[ANY]
key to [VALUES]
key for indexer property (for convenience and better Flow compatibility)
- 4.5.0
- Add
checker()
function to return the boolean checker function itself.
- 4.4.0
- Add
json
checker to check for JSON-friendly values (null, true, false, finite numbers, strings, plain objects, plain arrays)
- 4.3.0
- Add
circular
checker to check for objects with circular references - Add
!
modifier to enable invert checking, e.g. !num
(don't allow numbers) or !circular
(don't allow circular references)
- 4.2.2
- Use
.
dot notation in error message prefix when recursing into objects
- 4.2.1
- Fix bug where optional types were throwing an incorrect error message
- 4.2.0
- Rename
FormattedError
to ValueError
(more descriptive and reusable name) - Make
ValueError
the default error thrown by Blork (not ValueError)
- 4.1.0
- Allow custom error to be set for custom checkers via
add()
- Export
debug()
which allows any value to be converted to a string in a clean and clear format - Export
format()
which takes three arguments (message, value, prefix) and returns a consistently and beautifully formatted error message. - Export
FormattedError
which takes the same three arguments and applies format()
so it always has beautiful errors - Export
BlorkError
(which is thrown when you're using Blork wrong) for the purposes of checking thrown errors against it
- 4.0.0
- Major internal rewrite with API kept almost the same
- Add support for combining checkers with
|
and &
syntax check()
and args()
no longer return anything (previously returned the number of passing values)- Custom checkers should now return
boolean
(message/description for the checker can be passed in as third field to add()
)