tcomb
JavaScript types and combinators
Why. tcomb is a library which allows you to check the types of JavaScript values at runtime with a simple syntax. It is great for checking external input, for testing and for adding safety to your internal code. Bonus points:
- easy debugging
- encode/decode of your domain objects to/from JSON for free
- instances are immutables by default
How. This library provides several type combinators and a built-in assert
function you can use. When an assertion fails this library starts the debugger so you can inspect the stack and quickly find out what's wrong. The debugger starts only once after the first failed assert.
What. You can define and check:
- JavaScript native types
- structs (i.e. classes)
- unions
- maybe
- enums
- tuples
- subtypes
- lists
- function types (experimental)
Quick example
Let's build a product
var Product = struct({
name: Str, // a REQUIRED string
description: maybe(Str), // an OPTIONAL string (can be `null`)
homepage: Url, // a SUBTYPE of string representing an URL
shippings: list(Shipping), // a LIST of shipping methods
category: Category, // a string in [Audio, Video] (ENUM)
price: union(Num, Price), // price expressed in dollars OR in another currency (UNION)
dim: tuple([Num, Num]) // width x height (TUPLE)
});
var Url = subtype(Str, function (s) {
return s.indexOf('http://') === 0;
});
var Shipping = struct({
description: Str,
cost: Num
});
var Category = enums({
Audio: 0,
Video: 1
});
var Price = struct({
currency: Str,
amount: Num
});
// get an immutable instance
var ipod = new Product({
name: 'iPod',
description: 'Engineered for maximum funness.',
homepage: 'http://www.apple.com/ipod/',
shippings: [{
description: 'Shipped to your door, free.',
cost: 0
}],
category: 'Audio',
price: {currency: 'EUR', amount: 100},
dim: [2.4, 4.1]
});
Setup
Node
npm install tcomb
var t = require('tcomb');
Browser
This library uses a few ES5 methods
- Array#forEach()
- Array#map()
- Array#some()
- Array#every()
- Object#keys()
you can use es5-shim
to support old browsers
<!--[if lt IE 9]>
<script src="es5-shim.min.js"></script>
<![endif]-->
<script type="text/javascript" src="tcomb.js"></script>
<script type="text/javascript">
console.log(t);
</script>
Tests
Run
mocha
in the project root.
What's a type?
A type
is a function T
such that
T
has signature T(values, [mut])
where values
depends on the nature of T
and the optional boolean arg mut
makes the instance mutable (default false
)T
is idempotent: new T(new T(values)) "equals" new T(values)
T
owns a static function T.is(x)
returning true
if x
is a instance of T
Note: 2. implies that T
can be used as a default JSON decoder
Api
primitive(name, is)
Used internally to define JavaScript native types:
- Nil:
null
and undefined
- Str: strings
- Num: numbers
- Bool: booleans
- Arr: arrays
- Obj: plain objects
- Func: functions
- Err: errors
Example
Str.is('a'); // => true
Nil.is(undefined); // => true
Nil.is(0); // => false
struct(props, [name])
Defines a struct like type.
props
hash field name -> typename
optional string useful for debugging
Example
// define a struct with two numerical props
var Point = struct({
x: Num,
y: Num
});
Methods are defined as usual
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
Building an instance is simple as
"use strict";
var p = new Point({x: 1, y: 2});
p.x = 2; // => TypeError, p is immutable
p = new Point({x: 1, y: 2}, true); // now p is mutable
p.x = 2; // ok
Some meta informations are stored in a meta
hash
Point.meta = {
kind: 'struct',
props: props,
name: name
};
is(x)
Returns true
if x
is an instance of Point
.
Point.is(p); // => true
update(instance, updates, [mut])
Returns an instance with changed props, without modifying the original.
Point.update(p, {x: 3}); // => new Point({x: 3, y: 2})
union(types, [name])
Defines a types union.
types
array of typesname
optional string useful for debugging
Example
var Circle = struct({
center: Point,
radius: Num
});
var Rectangle = struct({
bl: Point, // bottom left vertex
tr: Point // top right vertex
});
var Shape = union([
Circle,
Rectangle
]);
// in order to use Shape as a constructor the function `dispatch(values) -> Type`
// must be implemented
Shape.dispatch = function (values) {
assert(Obj.is(values));
return values.hasOwnProperty('center') ?
Circle :
Rectangle;
};
var shape = new Shape({center: {x: 1, y: 2}, radius: 10});
Some meta informations are stored in a meta
hash
Shape.meta = {
kind: 'union',
types: types,
name: name
};
is(x)
Returns true
if x
belongs to the union.
Shape.is(new Circle({center: p, radius: 10})); // => true
maybe(type, [name])
Same as union([Nil, type])
.
var MaybeStr = maybe(Str);
MaybeStr.is('a'); // => true
MaybeStr.is(null); // => true
MaybeStr.is(1); // => false
Some meta informations are stored in a meta
hash
MaybeStr.meta = {
kind: 'maybe',
type: type,
name: name
};
enums(map, [name])
Defines an enum of strings.
var Direction = enums({
North: 0,
East: 1,
South: 2,
West: 3
});
Some meta informations are stored in a meta
hash
Direction.meta = {
kind: 'enums',
map: map,
name: name
};
is(x)
Returns true
if x
belongs to the enum.
Direction.is('North'); // => true
tuple(types, [name])
Defines a tuple whose coordinates have the specified types.
var Args = tuple([Num, Num]);
var a = new Args([1, 2]);
Some meta informations are stored in a meta
hash
Args.meta = {
kind: 'tuple',
types: types,
name: name
};
is(x)
Returns true
if x
belongs to the tuple.
Args.is([1, 2]); // => true
Args.is([1, 'a']); // => false, second element is not a Num
Args.is([1, 2, 3]); // => false, too many elements
update(instance, index, element, [mut])
Returns an instance without modifying the original.
Args.update(a, 0, 2); // => [2, 2]
subtype(type, predicate, [name])
Defines a subtype of an existing type.
var Int = subtype(Num, function (n) {
return n === parseInt(n, 10);
});
// points of the first quadrant
var Q1Point = subtype(Point, function (p) {
return p.x >= 0 && p.y >= 0;
});
// constructor usage
var p = new Q1Point({x: -1, y: -2}); // => fail!
Some meta informations are stored in a meta
hash
Int.meta = {
kind: 'subtype',
type: type,
predicate: predicate,
name: name
};
is(x)
Returns true
if x
belongs to the subtype.
Int.is(2); // => true
Int.is(1.1); // => false
list(type, [name])
Defines an array where all elements are of type type
.
var Path = list(Point);
// costructor usage
var path = new Path([
{x: 0, y: 0},
{x: 1, y: 1}
]);
Some meta informations are stored in a meta
hash
Path.meta = {
kind: 'list',
type: type,
name: name
};
is(x)
Returns true
if x
belongs to the list.
Path.is([{x: 0, y: 0}, {x: 1, y: 1}]); // => true
func(Arguments, f, [Return], [name])
Defines a function where the arguments
and the return value are checked.
var sum = func(tuple([Num, Num]), function (a, b) {
return a + b;
}, Num);
sum(1, 2); // => 3
sum(1, 'a'); // => fail!
Useful methods
Return an instance without modifying the original.
Path.append(path, element, [mut]);
Path.prepend(path, element, [mut]);
Path.update(path, index, element, [mut]);
Path.remove(path, index, [mut]);
Path.move(path, from, to, [mut]);
Utils
fail(message)
assert(guard, [message])
// make properties immutables unless `unless` is `true`
freeze(obj_or_arr, [unless])
// adds the properties of `y` to `x`. If `overwrite` is falsy
// and a property already exists, throws an error
mixin(x, y, [overwrite])
// array manipulation
append(arr, element);
prepend(arr, element);
update(arr, index, element);
remove(arr, index);
move(arr, from, to);
More examples
How to extend a struct
var Point3D = struct(mixin(Point.meta.props, {
z: Num
}));
var p = new Point3D({x: 1, y: 2, z: 3});
Deep update of a struct
var c = new Circle({center: {x: 1, y: 2}, radius: 10});
// translate x by 1
var c2 = Circle.update(c, {
center: Point.update(c.center, {
x: c.center.x + 1
})
});
Write a general JSON Decoder
// (json, T, mut) -> instance of T
function decode(json, T, mut) {
if (T.fromJSON) {
return T.fromJSON(json, mut);
}
switch (T.meta.kind) {
case 'struct' :
var values = {};
var props = T.meta.props;
for (var prop in props) {
if (props.hasOwnProperty(prop)) {
values[prop] = decode(json[prop], props[prop], mut);
}
}
return new T(values, mut);
case 'union' :
assert(Func.is(T.dispatch));
return decode(json, T.dispatch(json), mut);
case 'maybe' :
return Nil.is(json) ? undefined : decode(json, T.meta.type, mut);
case 'tuple' :
return freeze(json.map(function (x, i) {
return decode(x, T.meta.types[i], mut);
}, mut));
case 'subtype' :
var x = decode(json, T.meta.type, mut);
assert(T.meta.predicate(x));
return x;
case 'list' :
return freeze(json.map(function (x) {
return decode(x, T.meta.type, mut);
}), mut);
default :
return json;
}
};
Copyright & License
Copyright (C) 2014 Giulio Canti - Released under the MIT License.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.