"Si vis pacem, para bellum" (Vegetius 5th century)
tcomb is a library for Node.js and the browser which allows you to check the types of
JavaScript values at runtime with a simple syntax. It's great for Domain Driven Design,
for testing and for adding safety to your internal code.
The Idea
tcomb is based on set theory.
What's a type? In tcomb a type is a function T
such that
T
has signature T(value, [mut])
where value
depends on the nature of T
and the optional boolean mut
makes the instance mutable (default false
)T
is idempotent: T(T(value, mut), mut) === T(value, mut)
T
owns a static function T.is(x)
returning true
if x
is an instance of T
Note: 2. implies that T
can be used as a default JSON decoder
Articles on tcomb
Contributors
Contents
Features
- write complex domain models in a breeze and with small code footprint
- easy debugging
- instances are immutables by default
- encode/decode of domain models to/from JSON for free
The library provides a built-in assert
function, if an assert fails the debugger kicks in
so you can inspect the stack and quickly find out what's wrong.
You can handle:
JavaScript native types
- Nil:
null
and undefined
- Str: strings
- Num: numbers
- Bool: booleans
- Arr: arrays
- Obj: plain objects
- Func: functions
- Err: errors
- Re: regular expressions
- Dat: dates
- Any: *
type combinators (build new types from those already defined)
- struct (i.e. classes)
- union
- maybe
- enums
- tuple
- subtype
- list
- dict
- function type
Quick Examples
Let's build a product model
var Product = struct({
name: Str,
desc: maybe(Str),
home: Url,
shippings: list(Str),
category: Category,
price: Price,
size: tuple([Num, Num]),
warranty: dict(Str, Num)
});
var Url = subtype(Str, function (s) {
return s.indexOf('http://') === 0;
});
var Category = enums.of('audio video');
var ForeignPrice = struct({ currency: Str, amount: Num });
var Price = union([Num, ForeignPrice]);
Price.dispatch = function (x) {
return Num.is(x) ? Num : ForeignPrice;
};
var json = {
name: 'iPod',
desc: 'Engineered for maximum funness.',
home: 'http://www.apple.com/ipod/',
shippings: ['Same Day', 'Next Businness Day'],
category: 'audio',
price: {currency: 'EUR', amount: 100},
size: [2.4, 4.1],
warranty: {
US: 2,
IT: 1
}
};
var ipod = Product(json);
You have existing code and you want to add safety
function Point (x, y) {
this.x = x;
this.y = y;
}
var p = new Point(1, 'a');
in order to "tcombify" your code you can simply add some asserts
function Point (x, y) {
assert(Num.is(x));
assert(Num.is(y));
this.x = x;
this.y = y;
}
var p = new Point(1, 'a');
Setup
Node
npm install tcomb
Browser
bower install tcomb
or download the tcomb.min.js
file.
Requirements
This library uses a few ES5 methods, you can use es5-shim
, es5-sham
and json2
to support old browsers
Tests
Run mocha
or npm test
in the project root.
Api
options
options.onFail
In production envs you don't want to leak failures to the user
options.onFail = function (message) {
try {
throw new Error(message);
} catch (e) {
console.log(e.stack);
}
};
assert
assert(guard, [message], [values...]);
If guard !== true
the debugger kicks in.
guard
boolean conditionmessage
optional string useful for debugging, formatted with values like util.format in Node
Example
assert(1 === 2);
assert(1 === 2, 'error!');
assert(1 === 2, 'error: %s !== %s', 1, 2);
To customize failure behaviour, see options.onFail
.
structs
struct(props, [name])
Defines a struct like type.
props
hash name -> typename
optional string useful for debugging
Example
"use strict";
var Point = struct({
x: Num,
y: Num
});
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = Point({x: 1, y: 2});
p.x = 2;
p = Point({x: 1, y: 2}, true);
p.x = 2;
is(x)
Returns true
if x
is an instance of the struct.
Point.is(p);
extend(props, [name])
Returns a new type with the additional specified props
var Point = struct({
x: Num,
y: Num
}, 'Point');
var Point3D = Point.extend({z: Num}, 'Point3D');
var p = new Point3D({x: 1, y: 2, z: 3});
unions
union(types, [name])
Defines a union of types.
types
array of typesname
optional string useful for debugging
Example
var Circle = struct({
center: Point,
radius: Num
});
var Rectangle = struct({
bl: Point,
tr: Point
});
var Shape = union([
Circle,
Rectangle
]);
Shape.dispatch = function (x) {
return x.hasOwnProperty('center') ? Circle : Rectangle;
};
var circle = Shape({center: {x: 1, y: 0}, radius: 10});
var rectangle = Shape({bl: {x: 0, y: 0}, tr: {x: 1, y: 1}});
is(x)
Returns true
if x
belongs to the union.
Shape.is(new Circle({center: p, radius: 10}));
maybe
maybe(type, [name])
Same as union([Nil, type])
.
var Radio = maybe(Str);
Radio.is('a');
Radio.is(null);
Radio.is(1);
enums
enums(map, [name])
Defines an enum of strings.
map
hash enum -> valuename
optional string useful for debugging
Example
var Direction = enums({
North: 'North',
East: 'East',
South: 'South',
West: 'West'
});
is(x)
Returns true
if x
belongs to the enum.
Direction.is('North');
enums.of(keys, [name])
Returns an enums of an array of keys, useful when you don't mind to define
custom values for the enums.
keys
array (or string) of keysname
optional string useful for debugging
Example
var Direction = enums.of(['North', 'East', 'South', 'West']);
Direction = enums.of('North East South West');
tuples
tuple(types, [name])
Defines a tuple whose coordinates have the specified types.
types
array of coordinates typesname
optional string useful for debugging
Example
var Area = tuple([Num, Num]);
var area = Area([1, 2]);
0-tuples and 1-tuples are also supported
var Nothing = tuple([]);
var JustNum = tuple([Num]);
is(x)
Returns true
if x
belongs to the tuple.
Area.is([1, 2]);
Area.is([1, 'a']);
Area.is([1, 2, 3]);
subtypes
subtype(type, predicate, [name])
Defines a subtype of an existing type.
type
the supertypepredicate
a function with signature (x) -> boolean
name
optional string useful for debugging
Example
var Q1Point = subtype(Point, function (p) {
return p.x >= 0 && p.y >= 0;
});
var p = Q1Point({x: 1, y: 2});
p = Q1Point({x: -1, y: -2});
Note. You can't add methods to Q1Point
prototype
, add them to the supertype prototype
if needed.
is(x)
Returns true
if x
belongs to the subtype.
var Int = subtype(Num, function (n) {
return n === parseInt(n, 10);
});
Int.is(2);
Int.is(1.1);
lists
list(type, [name])
Defines an array where all the elements are of type T
.
type
type of all the elementsname
optional string useful for debugging
Example
var Path = list(Point);
var path = Path([
{x: 0, y: 0},
{x: 1, y: 1}
]);
is(x)
Returns true
if x
belongs to the list.
var p1 = Point({x: 0, y: 0});
var p2 = Point({x: 1, y: 2});
Path.is([p1, p2]);
dicts
dict(domain, codomain, [name])
Defines a dictionary domain -> codomain.
domain
the type of the keyscodomain
the type of the valuesname
optional string useful for debugging
Example
var Tel = dict(Str, Num);
is(x)
Returns true
if x
is an instance of the dict.
Tel.is({'jack': 4098, 'sape': 4139});
Updates
Type.update(instance, spec)
The following commands are compatible with the Facebook Immutability Helpers.
- $push
- $unshift
- $splice
- $set
- $apply
Removing a value form a dict
{$remove: keys}
var Type = dict(Str, Num);
var instance = Type({a: 1, b: 2});
var updated = Type.update(instance, {$remove: ['a']});
Swapping two list elements
{$swap: {from: number, to: number}}
var Type = list(Num);
var instance = Type([1, 2, 3, 4]);
var updated = Type.update(instance, {'$swap': {from: 1, to: 2}});
Adding other commands
You can add your custom commands updating the util.update.commands
hash.
functions
Typed functions may be defined like this:
var add = func([Num, Num], Num)
.of(function (x, y) { return x + y; });
And used like this:
add("Hello", 2);
add("Hello");
add(1, 2);
add(1)(2);
func(A, B, [name])
func(Domain, Codomain, name)
Returns a function type whose functions have their domain and codomain specified and constrained.
Domain
: the type of the function's argument (or list
of types of the function's arguments)Codomain
: the type of the function's return valuename
: optional string useful for debugging
func
can be used to define function types using native types:
var A = func(Str, Num);
The domain and codomain can also be specified using types from any combinator including func
:
var B = func(func(Str, Num), Str);
var ExcitedStr = subtype(Str, function (s) { return s.indexOf('!') !== -1; }, 'ExcitedStr');
var Exciter = func(Str, ExcitedStr);
Additionally the domain can be expressed as a list
of types:
var C = func([A, B, Str], Num);
.of(f)
func(A, B).of(f);
Returns a function where the domain and codomain are typechecked against the function type.
If the function is passed values which are outside of the domain or returns values which are outside of the codomain it will raise an error:
var simpleQuestionator = Exciter.of(function (s) { return s + '?'; });
var simpleExciter = Exciter.of(function (s) { return s + '!'; });
simpleQuestionator('Hello');
simpleExciter(1);
simpleExciter('Hello');
The returned function may also be partially applied:
var add = func([Num, Num], Num)
.of(function (x, y) { return x + y });
var addHello = add("Hello");
var add2 = add(2);
add2(1);
.is(x)
func(A, B).is(x);
Returns true if x belongs to the type.
Exciter.is(simpleExciter);
Exciter.is(simpleQuestionator);
var id = function (x) { return x; };
func([Num, Num], Num).is(func([Num, Num], Num).of(id));
func([Num, Num], Num).is(func(Num, Num).of(id));
Rules
- Typed functions' domains are checked when they are called
- Typed functions' codomains are checked when they return
- The domain and codomain of a typed function's type is checked when the typed function is passed to a function type (such as when used as an argument in another typed function).
Macros (experimental)
I added a tcomb.sjs
file containing some sweet.js macros, here some examples:
type Point struct {
x: Num,
y: Num
}
type Shape union {
Circle,
Rectangle
}
type Coords tuple {
Num,
Num
}
type Direction enums {
'North',
'East',
'South',
'West'
}
type Direction enums {
'North': 0,
'East': 1,
'South': 2,
'West': 3
}
type Positive subtype<Num> n {
return n > 0;
}
type Irriducible irriducible x {
return typeof x === 'function';
}
type Maybe maybe<Str>
type List list<Str>
type Dict dict<Str>
fn add (x: Num, y: Num) {
return x + y;
}
fn add (x: Num, y: Num) -> Num {
return x + y;
}
License
The MIT License (MIT)