assert-type v0.1.0
node-assert-type
npm install assert-type
One occasionally needs to design some JavaScript code conservatively, favoring correctness and safety over the language's natural strengths of flexibility and agility. Given JavaScript's dynamic typing, extensive runtime type assertions are a common product of such a conservative mindset. This node.js library makes it easier to write such assertions concisely. In addition to simple identifier-is-a-type assertions, it provides ways to algebraically compose those into more complex assertions about objects, arrays, and function signatures. A few examples may help illustrate:
var ty = require("assert-type");
var T = ty.Assert;
function deorbitBurn(power,seconds) {
T(ty.num.finite)(power); // assert power is a finite number (not NaN)
T(ty.int.nonneg)(seconds); // assert seconds is a nonnegative integer
// a more concise way to make the same two assertions:
T(ty.num.finite, ty.int.nonneg)(power, seconds);
...
}
function chooseTargetToBomb(targets) {
// assert targets is a nonempty array of nonempty strings
T(ty.arr.ne.of(ty.str.ne))(targets);
...
return T(ty.str.ne)(result); // assert we're returning a nonempty string
}
function getTRexFenceVoltage(fence) {
// assert that fence is an object with exactly two keys: sector, a nonempty
// string, and subsector, a non-negative integer.
T(ty.obj.of({sector: ty.str.ne, subsector: ty.int.nonneg}))(fence);
...
return T(ty.num.finite)(voltage); // assert we're returning a finite number
}
// Declare a function that takes a finite number and a boolean as arguments,
// and returns a finite number (finite*bool -> finite). Runtime assertions
// will fail if any of those types do not match, or if the wrong number of
// arguments is passed.
var F = ty.WrapFun;
var moveNuclearControlRods = F([ty.num.finite, ty.bool], ty.num.finite,
function(position, scram) {
...
return coreTemperature;
});
Manual
The examples all assume the preamble
var ty = require("assert-type");
Simple type predicates
At the foundation, the library provides simple functions that take a value and return a boolean indicating whether the value satisfies a specific type, essentially similar to those found in many other libraries. Don't worry, things get a bit more interesting down below.
bool
booleannum
number (includesNaN
)num.not.nan
any number butNaN
num.pos
positive real number (excludesNaN
)num.neg
num.nonneg
num.finite
real numberx
andNumber.NEGATIVE_INFINITY < x < Number.POSITIVE_INFINITY
num.finite.pos
num.finite.neg
num.finite.nonneg
int
integer (excludesNaN
and infinite)int.pos
int.neg
int.nonneg
str
stringstr.ne
nonempty stringarr
arrayarr.ne
nonempty arrayobj
object (includes null and arrays)obj.not.null
non-null object (includes arrays)inst.of(Cons)
testinstanceof Cons
(e.g.Array
,Date
, or your class)fun
functionfunN(n)
function ofn
argumentsnull
exactlynull
(mainly for use withor
)undefined
exactlyundefined
(ditto)any
always true
Clearly, the types are not mutually exclusive.
Assert
The Assert
function takes a type predicate and returns a function that takes
a value and asserts the value satisfies the predicate. If the assertion
succeeds, the value is also returned. This is easier to show by example:
var T = ty.Assert; // since we use it often
T(ty.bool)(true); // OK
T(ty.bool)(0); // throws ty.TypeAssertionError
var x = T(ty.num.finite)(0); // now x === 0
T(ty.num.finite)(NaN); // throws ty.TypeAssertionError
// OK:
T(ty.funN(1))(function (x) { return x; });
// throws ty.TypeAssertionError:
T(ty.funN(1))(function () { return 0; });
Assert
also understands multiple types followed by matching values:
T(ty.bool, ty.int)(true, 1); // OK
T(ty.bool, ty.int)(true, 1.1); // throws ty.TypeAssertionError
Composite types
The libary provides powerful operators for composing simple types into more complex ones.
or
or(ty1, ty2, ...)
returns a type predicate satisfied if any of the provided
predicates are satisfied:
T(ty.or(ty.int, ty.bool))(3); // OK
T(ty.or(ty.int, ty.bool))(true); // OK
T(ty.or(ty.int, ty.bool))(""); // throws ty.TypeAssertionError
arr.of and arr.ne.of
arr.of(somety)
returns a type predicate satisfied for arrays of the given
type.
T(ty.arr.of(ty.int))([1, 2, 3]); // OK
T(ty.arr.of(ty.int))([]); // vacuously OK
T(ty.arr.of(ty.int))([true]); // throws ty.TypeAssertionError
T(ty.arr.of(ty.int))(null); // throws ty.TypeAssertionError
arr.ne.of(somety)
rejects the empty array.
An alternative usage of arr.of
tests for an array with a specific number of
elements of specific types.
T(ty.arr.of([ty.int, ty.bool]))([1, true]); // OK
T(ty.arr.of([ty.int, ty.bool]))([1]); // throws ty.TypeAssertionError
T(ty.arr.of([ty.int, ty.bool]))([1, 2]); // throws ty.TypeAssertionError
obj.of and obj.with
obj.of(proto)
, where proto
is an object with keys mapping to types,
returns a type predicate testing whether an object has exactly those keys
and values satisfying the respective types.
T(ty.obj.of({x: ty.int, y: ty.int}))({x: 1, y: 2}); // OK
T(ty.obj.of({x: ty.int, y: ty.int}))({x: 1, y: true}); // throws ty.TypeAssertionError
T(ty.obj.of({x: ty.int, y: ty.int}))({x: 1}); // throws ty.TypeAssertionError
T(ty.obj.of({x: ty.int, y: ty.int}))({x: 1, y: 2, z: 3}); // throws ty.TypeAssertionError
obj.with(proto)
is similar, but permits and ignores keys in addition to
those specified in proto
.
Sum types
The composite types formed by the above operators can themselves be composed. For example, here's a simple sum type (tagged union) for 2D points in either Cartesian or polar coordinates:
var tCartesian = ty.obj.of({x: ty.num.finite, y: ty.num.finite});
var tPolar = ty.obj.of({r: ty.num.finite.nonneg, theta: ty.num.finite.nonneg});
var tPoint = ty.or(tCartesian, tPolar);
function distanceToOrigin(pt) {
T(tPoint)(pt);
if (tCartesian(pt)) {
return Math.sqrt(pt.x*pt.x, pt.y*pt.y);
} else if (tPolar(pt)) {
return pt.r;
} else {
// should never happen
assert(false);
}
}
As this example hints, types are values that can be assigned and passed around at runtime. But it is not currently possible to declare recursive types, limiting the scope of supported algebraic data types. I'm thinking about this.
Typed functions
fun.of([ty1, ty2, ...], tyRet)
is the type of a function whose arguments
have types ty1, ty2, ...
and which returns a value of tyRet
.
Functions with fun.of(...)
types can only be generated by WrapFun
, which
wraps a given function so that type assertions about its arguments and return
value are performed automatically before and after calling it.
var F = ty.WrapFun;
var t = ty.fun.of([ty.int, ty.bool], ty.int);
var intFn = F(t, function(x, b) {
if (b) return 0-x; else return x;
});
intFn(3, false); // returns 3
intFn(3, true); // returns -3
intFn(3, ""); // throws ty.TypeAssertionError
intFn(3); // throws ty.TypeAssertionError
// throws ty.TypeAssertionError due to wrong return value type:
F(t, function(x, b) { return true; })(3, false);
// OK:
T(t)(intFn);
// throws TypeAssertionError, since intFn has a different return type:
T(ty.fun.of([ty.int, ty.bool], ty.bool))(intFn);
intFn
could have been declared with the following shorter syntax, in which
the use of ty.fun.of
is implicit:
var intFn = F([ty.int, ty.bool], ty.int, function(x, b) {
if (b) return 0-x; else return x;
});
There's also a simple predicate fun.typed
to check whether a given function
was generated by WrapFun
. Any JavaScript function will satisfy fun
, any
function generated by WrapFun
will satisfy fun.typed
, and only functions
generated by WrapFun
with the exact same signature will satisfy
fun.of(...)
.
Typed functions with callbacks
There are a few ways to use WrapFun
with functions that return values
through callbacks (continuation passing style). First, if you're writing the
callback, you can guard the callback itself:
var F = ty.WrapFun;
var fs = require("fs");
fs.readFile("file.txt", "utf-8",
F([ty.any, ty.str], ty.any, function(err, txt) {
...
}));
One could further refine the signature of the callback to something like
ty.fun.of([ty.or(ty.undefined, ty.null, ty.inst.of(Error)), ty.str],
ty.undefined)
. In either case, a type mismatch of the return value would
result in a thrown ty.TypeAssertionError
.
Second, for writing a CPS function, there's a WrapFun_
variant for the
node.js asynchronous calling convention, where the callback is of the form
function (error, result) { ... }
.
var F_ = ty.WrapFun_;
var intFn_ = F_([ty.int, ty.bool, ty.fun], ty.int, function (x, b, cb) {
if (b) return cb(0-x); else return cb(x);
});
intFn_(3, false, function(error, result) {
// result === 3
});
intFn_(3, true, function(error, result) {
// result === -3
});
intFn_(3, "", function(error, result) {}); // throws ty.TypeAssertionError
F_([ty.fun], ty.int, function(cb) { return cb(true); })(function (error, result) {
// error is a ty.TypeAssertionError
});
WrapFun_
uses the return type to constrain the second argument to the
callback, and ignores the actual JavaScript return value of the function. Note
that the callback must still be represented explicitly as the last argument to
the function. As above, the type of the callback could be specified more
precisely, but this would require the callback to also be generated by
WrapFun
.
If a type assertion fails on the arguments, a regular JavaScript exception is thrown, since one can't be sure the callback is even present in this case. If the type assertion fails on the return value, the resulting error is passed to the callback.
There's also _WrapFun
for functions that take the callback as the first
argument rather than the last (useful for
streamline.js code).
Third and lastly, to write CPS functions not following the typical calling
convention, one can simply use the pass-through property of Assert
to guard
the return value. For example, a function whose callback only accepts the
return value:
var d = F([ty.num.finite, ty.num.finite, ty.fun], ty.any, function(x, y, cb) {
return cb(T(ty.num.finite)(Math.sqrt(x*x + y*y)));
});
Type introspection
The TypeOf, Name, and Describe functions make it possible to identify types at runtime. (I'm still thinking through these features, so they may evolve significantly.)
ty.Name(ty.TypeOf(0))); // => 'num'
ty.Name(ty.TypeOf(ty.Name)); // => 'fun.of([type], str.ne)'
ty.Describe(ty.TypeOf(ty.Describe)); // => '(type) -> nonempty string'
var myty = ty.fun.of([ty.arr.of(ty.int), ty.str.ne], ty.bool);
ty.Name(myty); // => 'fun.of([arr.of(int), str.ne], bool)'
ty.Describe(myty); // => '(integer array, nonempty string) -> boolean'
Wish list
- Optional function arguments and object fields
- Variadic functions and arrays
- Recursive types --> ADTs
- Polymorphic functions and type variables
- Flesh out obj.with as a module signature
11 years ago