assert-type v0.1.0
node-assert-type
npm install assert-typeOne 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.
boolbooleannumnumber (includesNaN)num.not.nanany number butNaNnum.pospositive real number (excludesNaN)num.negnum.nonnegnum.finitereal numberxandNumber.NEGATIVE_INFINITY < x < Number.POSITIVE_INFINITYnum.finite.posnum.finite.negnum.finite.nonnegintinteger (excludesNaNand infinite)int.posint.negint.nonnegstrstringstr.nenonempty stringarrarrayarr.nenonempty arrayobjobject (includes null and arrays)obj.not.nullnon-null object (includes arrays)inst.of(Cons)testinstanceof Cons(e.g.Array,Date, or your class)funfunctionfunN(n)function ofnargumentsnullexactlynull(mainly for use withor)undefinedexactlyundefined(ditto)anyalways 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.TypeAssertionErrorComposite 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.TypeAssertionErrorarr.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.TypeAssertionErrorarr.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.TypeAssertionErrorobj.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.TypeAssertionErrorobj.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
13 years ago