Repository: gcanti/tcomb
Pull request #125 (2015-08-24 17:38:23)

2.3


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 144e637..f4b7cb9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,48 @@
 **Note**: Gaps between patch versions are faulty/broken releases.
 **Note**: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.
 
+## v2.3.0
+
+- **New Feature**
+  - Add support for lazy messages in asserts, fix #124
+  - Better error messages for assert failures, fix #120
+
+  The messages now have the following general form:
+
+  ```
+  Invalid value <value> supplied to <context>
+  ```
+
+  where context is a slash-separated string with the following properties:
+
+  - the first element is the name of the "root"
+  - the following elements have the form: `<field name>: <field type>`
+
+  Note: for more readable messages remember to give types a name
+
+  Example:
+
+  ```js
+  var Person = t.struct({
+    name: t.String
+  }, 'Person'); // <- remember to give types a name
+
+  var User = t.struct({
+    email: t.String,
+    profile: Person
+  }, 'User');
+
+  var mynumber = t.Number('a');
+  // => Invalid value "a" supplied to Number
+
+  var myuser = User({ email: 1 });
+  // => Invalid value 1 supplied to User/email: String
+
+  myuser = User({ email: 'email', profile: { name: 2 } });
+  // => Invalid value 2 supplied to User/profile: Person/name: String
+  ```
+
+
 ## v2.2.1
 
 - **Experimental**
diff --git a/GUIDE.md b/GUIDE.md
index b605731..1c796fc 100644
--- a/GUIDE.md
+++ b/GUIDE.md
@@ -665,12 +665,12 @@ match(x: t.Any, cases...)
 where each case has the following structure
 
 ```
-type, [guard], result
+type, [guard], block
 ```
 
 - `type` a tcomb type
 - `guard` an optional predicate `(x) => t.Any`
-- `result` a function `(x) => t.Any` called when the match succeded
+- `block` a function `(x) => t.Any` called when the match succeded
 
 Example:
 
diff --git a/index.js b/index.js
index a25d58c..f37da8e 100644
--- a/index.js
+++ b/index.js
@@ -62,15 +62,16 @@ function is(x, type) {
   return isType(type) ? type.is(x) : isInstanceOf(x, type); // type should be a class constructor
 }
 
-function create(type, value) {
+function create(type, value, path) {
   if (isType(type)) {
     // for structs the new operator is allowed
-    return isStruct(type) ? new type(value) : type(value);
+    return isStruct(type) ? new type(value, path) : type(value, path);
   }
 
   if (process.env.NODE_ENV !== 'production') {
     // type should be a class constructor and value some instance, just check membership and return the value
-    assert(isInstanceOf(value, type), 'The value ' + exports.stringify(value) + ' is not an instance of ' + getFunctionName(type));
+    path = path || [getFunctionName(type)];
+    assert(isInstanceOf(value, type), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/'); });
   }
 
   return value;
@@ -92,7 +93,13 @@ exports.fail = function fail(message) {
 
 function assert(guard, message) {
   if (guard !== true) {
-    exports.fail(message || 'Assert failed');
+    if (isFunction(message)) {
+      message = message();
+    }
+    else if (isNil(message)) {
+      message = 'Assert failed (turn on "Pause on exceptions" in your Source panel)';
+    }
+    exports.fail(message);
   }
 }
 
@@ -103,7 +110,7 @@ function mixin(target, source, overwrite) {
     if (source.hasOwnProperty(k)) {
       if (overwrite !== true) {
         if (process.env.NODE_ENV !== 'production') {
-          assert(!target.hasOwnProperty(k), 'Invalid call to mixin(): cannot overwrite property ' + exports.stringify(k) + ' of target object');
+          assert(!target.hasOwnProperty(k), function () { return 'Invalid call to mixin(target, source, [overwrite]): cannot overwrite property "' + k + '" of target object'; });
         }
       }
       target[k] = source[k];
@@ -113,7 +120,7 @@ function mixin(target, source, overwrite) {
 }
 
 function forbidNewOperator(x, type) {
-  assert(!isInstanceOf(x, type), 'Cannot use the new operator to instantiate a type ' + getTypeName(type));
+  assert(!isInstanceOf(x, type), function () { return 'Cannot use the new operator to instantiate the type ' + getTypeName(type); });
 }
 
 function getShallowCopy(x) {
@@ -126,7 +133,7 @@ function getShallowCopy(x) {
 function update(instance, spec) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isObject(spec), 'Invalid argument spec = ' + exports.stringify(spec) + ' supplied to function update(instance, spec): expected an object containing commands');
+    assert(isObject(spec), function () { return 'Invalid argument spec ' + exports.stringify(spec) + ' supplied to function update(instance, spec): expected an object containing commands'; });
   }
 
   var value = getShallowCopy(instance);
@@ -147,23 +154,23 @@ function update(instance, spec) {
 
 function $apply(f, value) {
   if (process.env.NODE_ENV !== 'production') {
-    assert(isFunction(f), 'Invalid argument f supplied to immutability helper { $apply: f }: expected a function');
+    assert(isFunction(f), 'Invalid argument f supplied to immutability helper { $apply: f } (expected a function)');
   }
   return f(value);
 }
 
 function $push(elements, arr) {
   if (process.env.NODE_ENV !== 'production') {
-    assert(isArray(elements), 'Invalid argument elements supplied to immutability helper { $push: elements }: expected an array');
-    assert(isArray(arr), 'Invalid value supplied to immutability helper "$push": expected an array');
+    assert(isArray(elements), 'Invalid argument elements supplied to immutability helper { $push: elements } (expected an array)');
+    assert(isArray(arr), 'Invalid value supplied to immutability helper $push (expected an array)');
   }
   return arr.concat(elements);
 }
 
 function $remove(keys, obj) {
   if (process.env.NODE_ENV !== 'production') {
-    assert(isArray(keys), 'Invalid argument keys supplied to immutability helper { $remove: keys }: expected an array');
-    assert(isObject(obj), 'Invalid value supplied to immutability helper $remove: expected an object');
+    assert(isArray(keys), 'Invalid argument keys supplied to immutability helper { $remove: keys } (expected an array)');
+    assert(isObject(obj), 'Invalid value supplied to immutability helper $remove (expected an object)');
   }
   for (var i = 0, len = keys.length; i < len; i++ ) {
     delete obj[keys[i]];
@@ -177,8 +184,8 @@ function $set(value) {
 
 function $splice(splices, arr) {
   if (process.env.NODE_ENV !== 'production') {
-    assert(list(Arr).is(splices), 'Invalid argument splices supplied to immutability helper { $splice: splices }: expected an array of arrays');
-    assert(isArray(arr), 'Invalid value supplied to immutability helper $splice: expected an array');
+    assert(list(Arr).is(splices), 'Invalid argument splices supplied to immutability helper { $splice: splices } (expected an array of arrays)');
+    assert(isArray(arr), 'Invalid value supplied to immutability helper $splice (expected an array)');
   }
   return splices.reduce(function (acc, splice) {
     acc.splice.apply(acc, splice);
@@ -188,10 +195,10 @@ function $splice(splices, arr) {
 
 function $swap(config, arr) {
   if (process.env.NODE_ENV !== 'production') {
-    assert(isObject(config), 'Invalid argument config supplied to immutability helper { $swap: config }: expected an object');
-    assert(isNumber(config.from), 'Invalid argument config.from supplied to immutability helper { $swap: config }: expected a number');
-    assert(isNumber(config.to), 'Invalid argument config.to supplied to immutability helper { $swap: config }: expected a number');
-    assert(isArray(arr), 'Invalid value supplied to immutability helper $swap');
+    assert(isObject(config), 'Invalid argument config supplied to immutability helper { $swap: config } (expected an object)');
+    assert(isNumber(config.from), 'Invalid argument config.from supplied to immutability helper { $swap: config } (expected a number)');
+    assert(isNumber(config.to), 'Invalid argument config.to supplied to immutability helper { $swap: config } (expected a number)');
+    assert(isArray(arr), 'Invalid value supplied to immutability helper $swap (expected an array)');
   }
   var element = arr[config.to];
   arr[config.to] = arr[config.from];
@@ -201,8 +208,8 @@ function $swap(config, arr) {
 
 function $unshift(elements, arr) {
   if (process.env.NODE_ENV !== 'production') {
-    assert(isArray(elements), 'Invalid argument elements supplied to immutability helper {$unshift: elements}');
-    assert(isArray(arr), 'Invalid value supplied to immutability helper $unshift');
+    assert(isArray(elements), 'Invalid argument elements supplied to immutability helper {$unshift: elements} (expected an array)');
+    assert(isArray(arr), 'Invalid value supplied to immutability helper $unshift (expected an array)');
   }
   return elements.concat(arr);
 }
@@ -225,15 +232,16 @@ update.commands = {
 function irreducible(name, predicate) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isString(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to irreducible(name, predicate)');
+    assert(isString(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to irreducible(name, predicate) (expected a string)'; });
     assert(isFunction(predicate), 'Invalid argument predicate supplied to irreducible(name, predicate)');
   }
 
-  function Irreducible(value) {
+  function Irreducible(value, path) {
 
     if (process.env.NODE_ENV !== 'production') {
       forbidNewOperator(this, Irreducible);
-      assert(predicate(value), 'Invalid argument value = ' + exports.stringify(value) + ' supplied to irreducible type ' + name);
+      path = path || [name];
+      assert(predicate(value), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/'); });
     }
 
     return value;
@@ -290,20 +298,21 @@ function getDefaultStructName(props) {
 function struct(props, name) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(dict(Str, Func).is(props), 'Invalid argument props = ' + exports.stringify(props) + ' supplied to struct(props, name): expected a dictionary of tcomb types');
-    assert(isTypeName(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to struct(props, name): expected a string');
+    assert(dict(Str, Func).is(props), function () { return 'Invalid argument props ' + exports.stringify(props) + ' supplied to struct(props, [name]) combinator (expected a dictionary String -> Type)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to struct(props, [name]) combinator (expected a string)'; });
   }
 
   var displayName = name || getDefaultStructName(props);
 
-  function Struct(value) {
+  function Struct(value, path) {
 
     if (Struct.is(value)) { // makes Struct idempotent
       return value;
     }
 
     if (process.env.NODE_ENV !== 'production') {
-      assert(isObject(value), 'Invalid argument value = ' + exports.stringify(value) + ' supplied to struct ' + displayName + ': expected an object');
+      path = path || [displayName];
+      assert(isObject(value), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/') + ' (expected an object)'; });
     }
 
     if (!isInstanceOf(this, Struct)) { // makes `new` optional
@@ -314,7 +323,7 @@ function struct(props, name) {
       if (props.hasOwnProperty(k)) {
         var expected = props[k];
         var actual = value[k];
-        this[k] = create(expected, actual);
+        this[k] = create(expected, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(k + ': ' + getTypeName(expected)) : null ));
       }
     }
 
@@ -346,7 +355,7 @@ function struct(props, name) {
         return x;
       }
       if (process.env.NODE_ENV !== 'production') {
-        assert(isStruct(x), 'Invalid argument structs[' + i + '] = ' + exports.stringify(structs[i]) + ' supplied to ' + displayName + '.extend(structs, name)');
+        assert(isStruct(x), function () { return 'Invalid argument structs[' + i + '] ' + exports.stringify(structs[i]) + ' supplied to ' + displayName + '.extend(structs, name)'; });
       }
       return x.meta.props;
     });
@@ -366,26 +375,26 @@ function getDefaultUnionName(types) {
 function union(types, name) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isArray(types) && types.every(isFunction) && types.length >= 2, 'Invalid argument types = ' + exports.stringify(types) + ' supplied to union(types, name): expected an array of at least 2 types');
-    assert(isTypeName(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to union(types, name): expected a string');
+    assert(isArray(types) && types.every(isFunction) && types.length >= 2, function () { return 'Invalid argument types ' + exports.stringify(types) + ' supplied to union(types, [name]) combinator (expected an array of at least 2 types)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to union(types, [name]) combinator (expected a string)'; });
   }
 
   var displayName = name || getDefaultUnionName(types);
 
-  function Union(value) {
+  function Union(value, path) {
 
     if (process.env.NODE_ENV !== 'production') {
       forbidNewOperator(this, Union);
-      assert(isFunction(Union.dispatch), 'Unimplemented dispatch() function for union ' + displayName);
     }
 
     var type = Union.dispatch(value);
 
     if (process.env.NODE_ENV !== 'production') {
-      assert(isType(type), 'The dispatch() function of union ' + displayName + ' returns no type');
+      path = path || [displayName];
+      assert(isType(type), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/'); });
     }
 
-    return create(type, value);
+    return create(type, value, path);
   }
 
   Union.meta = {
@@ -403,7 +412,7 @@ function union(types, name) {
   };
 
   Union.dispatch = function (x) { // default dispatch implementation
-    for (var i = 0; i < types.length; i++ ) {
+    for (var i = 0, len = types.length; i < len; i++ ) {
       if (is(x, types[i])) {
         return types[i];
       }
@@ -420,20 +429,18 @@ function getDefaultIntersectionName(types) {
 function intersection(types, name) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isArray(types) && types.every(isFunction) && types.length >= 2, 'Invalid argument types = ' + exports.stringify(types) + ' supplied to intersection(types, name): expected an array of at least 2 types');
-    assert(isTypeName(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to intersection(types, name): expected a string');
+    assert(isArray(types) && types.every(isFunction) && types.length >= 2, function () { return 'Invalid argument types ' + exports.stringify(types) + ' supplied to intersection(types, [name]) combinator (expected an array of at least 2 types)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to intersection(types, [name]) combinator (expected a string)'; });
   }
 
   var displayName = name || getDefaultIntersectionName(types);
 
-  function Intersection(value) {
+  function Intersection(value, path) {
 
     if (process.env.NODE_ENV !== 'production') {
       forbidNewOperator(this, Intersection);
-    }
-
-    if (process.env.NODE_ENV !== 'production') {
-      assert(Intersection.is(value), 'Invalid argument value = ' + exports.stringify(value) + ' supplied to intersection ' + displayName);
+      path = path || [displayName];
+      assert(Intersection.is(value), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/'); });
     }
 
     return value;
@@ -467,7 +474,7 @@ function getDefaultMaybeName(type) {
 function maybe(type, name) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isFunction(type), 'Invalid argument type = ' + exports.stringify(type) + ' supplied to maybe(type, name): expected a type');
+    assert(isFunction(type), function () { return 'Invalid argument type ' + exports.stringify(type) + ' supplied to maybe(type, [name]) combinator (expected a type)'; });
   }
 
   if (isMaybe(type) || type === Any || type === Nil) { // makes the combinator idempotent and handle Any, Nil
@@ -475,16 +482,16 @@ function maybe(type, name) {
   }
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isTypeName(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to maybe(type, name): expected a string');
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to maybe(type, [name]) combinator (expected a string)'; });
   }
 
   name = name || getDefaultMaybeName(type);
 
-  function Maybe(value) {
+  function Maybe(value, path) {
     if (process.env.NODE_ENV !== 'production') {
       forbidNewOperator(this, Maybe);
     }
-    return isNil(value) ? null : create(type, value);
+    return isNil(value) ? null : create(type, value, path);
   }
 
   Maybe.meta = {
@@ -509,17 +516,20 @@ function getDefaultEnumsName(map) {
 function enums(map, name) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isObject(map), 'Invalid argument map = ' + exports.stringify(map) + ' supplied to enums(map, name): expected a hash of strings / numbers');
-    assert(isTypeName(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to enums(map, name): expected a string');
+    assert(isObject(map), function () { return 'Invalid argument map ' + exports.stringify(map) + ' supplied to enums(map, [name]) combinator (expected a dictionary of String -> String | Number)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to enums(map, [name]) combinator (expected a string)'; });
   }
 
   var displayName = name || getDefaultEnumsName(map);
 
-  function Enums(value) {
+  function Enums(value, path) {
+
     if (process.env.NODE_ENV !== 'production') {
       forbidNewOperator(this, Enums);
-      assert(Enums.is(value), 'Invalid argument value = ' + exports.stringify(value) + ' supplied to enums ' + displayName + ': expected one of ' + exports.stringify(Object.keys(map)));
+      path = path || [displayName];
+      assert(Enums.is(value), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/') + ' (expected one of ' + exports.stringify(Object.keys(map)) + ')'; });
     }
+
     return value;
   }
 
@@ -554,8 +564,8 @@ function getDefaultTupleName(types) {
 function tuple(types, name) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isArray(types) && types.every(isFunction), 'Invalid argument types = ' + exports.stringify(types) + ' supplied to tuple(types, name): expected an array of types');
-    assert(isTypeName(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to tuple(types, name): expected a string');
+    assert(isArray(types) && types.every(isFunction), function () { return 'Invalid argument types ' + exports.stringify(types) + ' supplied to tuple(types, [name]) combinator (expected an array of types)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to tuple(types, [name]) combinator (expected a string)'; });
   }
 
   var displayName = name || getDefaultTupleName(types);
@@ -566,10 +576,11 @@ function tuple(types, name) {
     });
   }
 
-  function Tuple(value) {
+  function Tuple(value, path) {
 
     if (process.env.NODE_ENV !== 'production') {
-      assert(isArray(value) && value.length === types.length, 'Invalid argument value = ' + exports.stringify(value) + ' supplied to tuple ' + displayName + ': expected an array of length ' + types.length);
+      path = path || [displayName];
+      assert(isArray(value) && value.length === types.length, function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/') + ' (expected an array of length ' + types.length + ')'; });
     }
 
     if (isTuple(value)) { // makes Tuple idempotent
@@ -580,10 +591,10 @@ function tuple(types, name) {
     }
 
     var arr = [], expected, actual;
-    for (var i = 0; i < types.length; i++) {
+    for (var i = 0, len = types.length; i < len; i++) {
       expected = types[i];
       actual = value[i];
-      arr.push(create(expected, actual));
+      arr.push(create(expected, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(i + ': ' + getTypeName(expected)) : null )));
     }
 
     if (process.env.NODE_ENV !== 'production') {
@@ -621,23 +632,24 @@ function getDefaultSubtypeName(type, predicate) {
 function subtype(type, predicate, name) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isFunction(type), 'Invalid argument type = ' + exports.stringify(type) + ' supplied to subtype(type, predicate, name): expected a type');
-    assert(isFunction(predicate), 'Invalid argument predicate supplied to subtype(type, predicate, name): expected a function');
-    assert(isTypeName(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to subtype(type, predicate, name): expected a string');
+    assert(isFunction(type), function () { return 'Invalid argument type ' + exports.stringify(type) + ' supplied to subtype(type, predicate, [name]) combinator (expected a type)'; });
+    assert(isFunction(predicate), function () { return 'Invalid argument predicate supplied to subtype(type, predicate, [name]) combinator (expected a function)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to subtype(type, predicate, [name]) combinator (expected a string)'; });
   }
 
   var displayName = name || getDefaultSubtypeName(type, predicate);
 
-  function Subtype(value) {
+  function Subtype(value, path) {
 
     if (process.env.NODE_ENV !== 'production') {
       forbidNewOperator(this, Subtype);
+      path = path || [displayName];
     }
 
-    var x = create(type, value);
+    var x = create(type, value, path);
 
     if (process.env.NODE_ENV !== 'production') {
-      assert(predicate(x), 'Invalid argument value = ' + exports.stringify(value) + ' supplied to subtype ' + displayName);
+      assert(predicate(x), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/'); });
     }
 
     return x;
@@ -670,11 +682,12 @@ function getDefaultListName(type) {
 function list(type, name) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isFunction(type), 'Invalid argument type = ' + exports.stringify(type) + ' supplied to list(type, name): expected a type');
-    assert(isTypeName(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to list(type, name): expected a string');
+    assert(isFunction(type), function () { return 'Invalid argument type ' + exports.stringify(type) + ' supplied to list(type, [name]) combinator (expected a type)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to list(type, [name]) combinator (expected a string)'; });
   }
 
   var displayName = name || getDefaultListName(type);
+  var typeNameCache = getTypeName(type);
 
   function isList(x) {
     return x.every(function (e) {
@@ -682,10 +695,11 @@ function list(type, name) {
     });
   }
 
-  function List(value) {
+  function List(value, path) {
 
     if (process.env.NODE_ENV !== 'production') {
-      assert(isArray(value), 'Invalid argument value = ' + exports.stringify(value) + ' supplied to list ' + displayName);
+      assert(isArray(value), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + displayName + ' (expected an array of ' + typeNameCache + ')'; });
+      path = path || [displayName];
     }
 
     if (isList(value)) { // makes List idempotent
@@ -698,7 +712,7 @@ function list(type, name) {
     var arr = [];
     for (var i = 0, len = value.length; i < len; i++ ) {
       var actual = value[i];
-      arr.push(create(type, actual));
+      arr.push(create(type, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(i + ': ' + typeNameCache) : null )));
     }
 
     if (process.env.NODE_ENV !== 'production') {
@@ -734,12 +748,14 @@ function getDefaultDictName(domain, codomain) {
 function dict(domain, codomain, name) {
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(isFunction(domain), 'Invalid argument domain = ' + exports.stringify(domain) + ' supplied to dict(domain, codomain, name): expected a type');
-    assert(isFunction(codomain), 'Invalid argument codomain = ' + exports.stringify(codomain) + ' supplied to dict(domain, codomain, name): expected a type');
-    assert(isTypeName(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to dict(domain, codomain, name): expected a string');
+    assert(isFunction(domain), function () { return 'Invalid argument domain ' + exports.stringify(domain) + ' supplied to dict(domain, codomain, [name]) combinator (expected a type)'; });
+    assert(isFunction(codomain), function () { return 'Invalid argument codomain ' + exports.stringify(codomain) + ' supplied to dict(domain, codomain, [name]) combinator (expected a type)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to dict(domain, codomain, [name]) combinator (expected a string)'; });
   }
 
   var displayName = name || getDefaultDictName(domain, codomain);
+  var domainNameCache = getTypeName(domain);
+  var codomainNameCache = getTypeName(codomain);
 
   function isDict(x) {
     for (var k in x) {
@@ -752,10 +768,11 @@ function dict(domain, codomain, name) {
     return true;
   }
 
-  function Dict(value) {
+  function Dict(value, path) {
 
     if (process.env.NODE_ENV !== 'production') {
-      assert(isObject(value), 'Invalid argument value = ' + exports.stringify(value) + ' supplied to dict ' + displayName);
+      assert(isObject(value), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + displayName; });
+      path = path || [displayName];
     }
 
     if (isDict(value)) { // makes Dict idempotent
@@ -768,9 +785,9 @@ function dict(domain, codomain, name) {
     var obj = {};
     for (var k in value) {
       if (value.hasOwnProperty(k)) {
-        k = create(domain, k);
+        k = create(domain, k, ( process.env.NODE_ENV !== 'production' ? path.concat(domainNameCache) : null ));
         var actual = value[k];
-        obj[k] = create(codomain, actual);
+        obj[k] = create(codomain, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(k + ': ' + codomainNameCache) : null ));
       }
     }
 
@@ -814,9 +831,9 @@ function func(domain, codomain, name) {
   domain = isArray(domain) ? domain : [domain]; // handle handy syntax for unary functions
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(list(Func).is(domain), 'Invalid argument domain = ' + exports.stringify(domain) + ' supplied to func(domain, codomain, name): expected an array of types');
-    assert(isFunction(codomain), 'Invalid argument codomain = ' + exports.stringify(codomain) + ' supplied to func(domain, codomain, name): expected a type');
-    assert(isTypeName(name), 'Invalid argument name = ' + exports.stringify(name) + ' supplied to func(domain, codomain, name): expected a string');
+    assert(list(Func).is(domain), function () { return 'Invalid argument domain ' + exports.stringify(domain) + ' supplied to func(domain, codomain, [name]) combinator (expected an array of types)'; });
+    assert(isFunction(codomain), function () { return 'Invalid argument codomain ' + exports.stringify(codomain) + ' supplied to func(domain, codomain, [name]) combinator (expected a type)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to func(domain, codomain, [name]) combinator (expected a string)'; });
   }
 
   var displayName = name || getDefaultFuncName(domain, codomain);
@@ -828,7 +845,7 @@ function func(domain, codomain, name) {
     }
 
     if (process.env.NODE_ENV !== 'production') {
-      assert(FuncType.is(value), 'Invalid argument value = ' + exports.stringify(value) + ' supplied to func ' + displayName);
+      assert(FuncType.is(value), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + displayName; });
     }
 
     return value;
@@ -855,8 +872,8 @@ function func(domain, codomain, name) {
   FuncType.of = function (f, curried) {
 
     if (process.env.NODE_ENV !== 'production') {
-      assert(isFunction(f), 'Invalid argument f supplied to func.of ' + displayName + ': expected a function');
-      assert(isNil(curried) || isBoolean(curried), 'Invalid argument curried = ' + exports.stringify(curried) + ' supplied to func.of ' + displayName + ': expected a boolean');
+      assert(isFunction(f), function () { return 'Invalid argument f supplied to func.of ' + displayName + ' (expected a function)'; });
+      assert(isNil(curried) || isBoolean(curried), function () { return 'Invalid argument curried ' + exports.stringify(curried) + ' supplied to func.of ' + displayName + ' (expected a boolean)'; });
     }
 
     if (FuncType.is(f)) { // makes FuncType.of idempotent
@@ -916,9 +933,9 @@ function match(x) {
 
     if (process.env.NODE_ENV !== 'production') {
       count = (count || 0) + 1;
-      assert(isType(type), 'Invalid type in clause #' + count);
-      assert(isFunction(guard), 'Invalid guard in clause #' + count + '');
-      assert(isFunction(f), 'Invalid block in clause #' + count + '');
+      assert(isType(type), function () { return 'Invalid type in clause #' + count; });
+      assert(isFunction(guard), function () { return 'Invalid guard in clause #' + count; });
+      assert(isFunction(f), function () { return 'Invalid block in clause #' + count; });
     }
 
     if (type.is(x) && guard(x)) {
diff --git a/package.json b/package.json
index 08e52bd..be2d34b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "tcomb",
-  "version": "2.2.1",
+  "version": "2.3.0",
   "description": "Type checking and DDD for JavaScript",
   "main": "index.js",
   "scripts": {
@@ -26,19 +26,25 @@
     "combinators",
     "checking",
     "safety",
-    "models",
+    "model",
     "domain",
     "debugging",
-    "immutable"
+    "immutable",
+    "DDD",
+    "JSON",
+    "store"
   ],
   "keywords": [
     "type",
     "combinators",
     "checking",
     "safety",
-    "models",
+    "model",
     "domain",
     "debugging",
-    "immutable"
+    "immutable",
+    "DDD",
+    "JSON",
+    "store"
   ]
 }


Test patch:
diff --git a/test/assert.js b/test/assert.js
new file mode 100644
index 0000000..5277817
--- /dev/null
+++ b/test/assert.js
@@ -0,0 +1,45 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+describe('t.assert(guard, [message])', function () {
+
+  it('should nor throw when guard is true', function () {
+    t.assert(true);
+  });
+
+  it('should throw a default message', function () {
+    throwsWithMessage(function () {
+      t.assert(1 === 2); // eslint-disable-line
+    }, '[tcomb] Assert failed (turn on "Pause on exceptions" in your Source panel)');
+  });
+
+  it('should throw the specified message', function () {
+    throwsWithMessage(function () {
+      t.assert(1 === 2, 'my message'); // eslint-disable-line
+    }, '[tcomb] my message');
+  });
+
+  it('should handle lazy messages', function () {
+    throwsWithMessage(function () {
+      t.assert(1 === 2, function () { return 'lazy'; }); // eslint-disable-line
+    }, '[tcomb] lazy');
+  });
+
+  it('should handle custom fail behaviour', function () {
+    var fail = t.fail;
+    t.fail = function (message) {
+      try {
+        throw new Error(message);
+      } catch (e) {
+        assert.strictEqual(e.message, 'report error');
+      }
+    };
+    assert.doesNotThrow(function () {
+      t.assert(1 === 2, 'report error'); // eslint-disable-line
+    });
+    t.fail = fail;
+  });
+
+});
diff --git a/test/default-irreducibles.js b/test/default-irreducibles.js
new file mode 100644
index 0000000..417568f
--- /dev/null
+++ b/test/default-irreducibles.js
@@ -0,0 +1,362 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+var noop = function () {};
+var ko = function (x, message) { assert.strictEqual(x, false, message); };
+
+describe('t.Any', function () {
+
+  var T = t.Any;
+
+  describe('constructor', function () {
+
+    it('should behave like identity', function () {
+      assert.deepEqual(t.Any('a'), 'a');
+    });
+
+    it('should throw if used with new', function () {
+      throwsWithMessage(function () {
+        var x = new T(); // eslint-disable-line
+      }, '[tcomb] Cannot use the new operator to instantiate the type Any');
+    });
+
+  });
+
+  describe('#is(x)', function () {
+
+    it('should always return true', function () {
+      assert.ok(T.is(null));
+      assert.ok(T.is(undefined));
+      assert.ok(T.is(0));
+      assert.ok(T.is(true));
+      assert.ok(T.is(''));
+      assert.ok(T.is([]));
+      assert.ok(T.is({}));
+      assert.ok(T.is(noop));
+      assert.ok(T.is(/a/));
+      assert.ok(T.is(new RegExp('a')));
+      assert.ok(T.is(new Error()));
+    });
+
+  });
+
+});
+
+//
+// irreducible types
+//
+
+describe('irreducibles types', function () {
+
+  it('should throw if used with an invalid name argument', function () {
+    throwsWithMessage(function () {
+      t.irreducible(null, function () { return true; });
+    }, '[tcomb] Invalid argument name null supplied to irreducible(name, predicate) (expected a string)');
+  });
+
+  [
+    {T: t.Nil, x: null},
+    {T: t.String, x: 'a'},
+    {T: t.Number, x: 1},
+    {T: t.Boolean, x: true},
+    {T: t.Array, x: []},
+    {T: t.Object, x: {}},
+    {T: t.Function, x: noop},
+    {T: t.Error, x: new Error()},
+    {T: t.RegExp, x: /a/},
+    {T: t.Date, x: new Date()}
+  ].forEach(function (o) {
+
+    var T = o.T;
+    var x = o.x;
+
+    it('should accept only valid values', function () {
+      assert.deepEqual(T(x), x);
+    });
+
+    it('should throw if used with new', function () {
+      throwsWithMessage(function () {
+        var x = new T(); // eslint-disable-line
+      }, '[tcomb] Cannot use the new operator to instantiate the type ' + t.getTypeName(T));
+    });
+
+  });
+
+});
+
+describe('t.Nil', function () {
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is null or undefined', function () {
+      assert.ok(t.Nil.is(null));
+      assert.ok(t.Nil.is(undefined));
+    });
+
+    it('should return false when x is neither null nor undefined', function () {
+      ko(t.Nil.is(0));
+      ko(t.Nil.is(true));
+      ko(t.Nil.is(''));
+      ko(t.Nil.is([]));
+      ko(t.Nil.is({}));
+      ko(t.Nil.is(noop));
+      ko(t.Nil.is(new Error()));
+      ko(t.Nil.is(new Date()));
+      ko(t.Nil.is(/a/));
+      ko(t.Nil.is(new RegExp('a')));
+    });
+
+  });
+
+});
+
+describe('t.Boolean', function () {
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is true or false', function () {
+      assert.ok(t.Boolean.is(true));
+      assert.ok(t.Boolean.is(false));
+    });
+
+    it('should return false when x is neither true nor false', function () {
+      ko(t.Boolean.is(null));
+      ko(t.Boolean.is(undefined));
+      ko(t.Boolean.is(0));
+      ko(t.Boolean.is(''));
+      ko(t.Boolean.is([]));
+      ko(t.Boolean.is({}));
+      ko(t.Boolean.is(noop));
+      ko(t.Boolean.is(/a/));
+      ko(t.Boolean.is(new RegExp('a')));
+      ko(t.Boolean.is(new Error()));
+      ko(t.Boolean.is(new Date()));
+    });
+
+  });
+
+});
+
+describe('t.Number', function () {
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is a number', function () {
+      assert.ok(t.Number.is(0));
+      assert.ok(t.Number.is(1));
+      ko(t.Number.is(new Number(1))); // eslint-disable-line
+    });
+
+    it('should return false when x is not a number', function () {
+      ko(t.Number.is(NaN));
+      ko(t.Number.is(Infinity));
+      ko(t.Number.is(-Infinity));
+      ko(t.Number.is(null));
+      ko(t.Number.is(undefined));
+      ko(t.Number.is(true));
+      ko(t.Number.is(''));
+      ko(t.Number.is([]));
+      ko(t.Number.is({}));
+      ko(t.Number.is(noop));
+      ko(t.Number.is(/a/));
+      ko(t.Number.is(new RegExp('a')));
+      ko(t.Number.is(new Error()));
+      ko(t.Number.is(new Date()));
+    });
+
+  });
+
+});
+
+describe('t.String', function () {
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is a string', function () {
+      assert.ok(t.String.is(''));
+      assert.ok(t.String.is('a'));
+      /* jshint ignore:start */
+      ko(t.String.is(new String('a'))); // eslint-disable-line
+      /* jshint ignore:end */
+    });
+
+    it('should return false when x is not a string', function () {
+      ko(t.String.is(NaN));
+      ko(t.String.is(Infinity));
+      ko(t.String.is(-Infinity));
+      ko(t.String.is(null));
+      ko(t.String.is(undefined));
+      ko(t.String.is(true));
+      ko(t.String.is(1));
+      ko(t.String.is([]));
+      ko(t.String.is({}));
+      ko(t.String.is(noop));
+      ko(t.String.is(/a/));
+      ko(t.String.is(new RegExp('a')));
+      ko(t.String.is(new Error()));
+      ko(t.String.is(new Date()));
+    });
+
+  });
+
+});
+
+describe('t.Array', function () {
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is an array', function () {
+      assert.ok(t.Array.is([]));
+    });
+
+    it('should return false when x is not an array', function () {
+      ko(t.Array.is(NaN));
+      ko(t.Array.is(Infinity));
+      ko(t.Array.is(-Infinity));
+      ko(t.Array.is(null));
+      ko(t.Array.is(undefined));
+      ko(t.Array.is(true));
+      ko(t.Array.is(1));
+      ko(t.Array.is('a'));
+      ko(t.Array.is({}));
+      ko(t.Array.is(noop));
+      ko(t.Array.is(/a/));
+      ko(t.Array.is(new RegExp('a')));
+      ko(t.Array.is(new Error()));
+      ko(t.Array.is(new Date()));
+    });
+
+  });
+
+});
+
+describe('t.Object', function () {
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is an object', function () {
+      function A() {}
+      assert.ok(t.Object.is({}));
+      assert.ok(t.Object.is(new A()));
+    });
+
+    it('should return false when x is not an object', function () {
+      ko(t.Object.is(null));
+      ko(t.Object.is(undefined));
+      ko(t.Object.is(0));
+      ko(t.Object.is(''));
+      ko(t.Object.is([]));
+      ko(t.Object.is(noop));
+    });
+
+  });
+
+});
+
+describe('t.Function', function () {
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is a function', function () {
+      assert.ok(t.Function.is(noop));
+      assert.ok(t.Function.is(new Function())); // eslint-disable-line
+    });
+
+    it('should return false when x is not a function', function () {
+      ko(t.Function.is(null));
+      ko(t.Function.is(undefined));
+      ko(t.Function.is(0));
+      ko(t.Function.is(''));
+      ko(t.Function.is([]));
+      ko(t.Function.is({}));
+      ko(t.Function.is(new String('1'))); // eslint-disable-line
+      ko(t.Function.is(new Number(1))); // eslint-disable-line
+      ko(t.Function.is(new Boolean())); // eslint-disable-line
+      ko(t.Function.is(/a/));
+      ko(t.Function.is(new RegExp('a')));
+      ko(t.Function.is(new Error()));
+      ko(t.Function.is(new Date()));
+    });
+
+  });
+
+});
+
+describe('t.Error', function () {
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is an error', function () {
+      assert.ok(t.Error.is(new Error()));
+    });
+
+    it('should return false when x is not an error', function () {
+      ko(t.Error.is(null));
+      ko(t.Error.is(undefined));
+      ko(t.Error.is(0));
+      ko(t.Error.is(''));
+      ko(t.Error.is([]));
+      ko(t.Error.is(new String('1'))); // eslint-disable-line
+      ko(t.Error.is(new Number(1))); // eslint-disable-line
+      ko(t.Error.is(new Boolean())); // eslint-disable-line
+      ko(t.Error.is(/a/));
+      ko(t.Error.is(new RegExp('a')));
+      ko(t.Error.is(new Date()));
+    });
+
+  });
+
+});
+
+describe('t.RegExp', function () {
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is a regexp', function () {
+      assert.ok(t.RegExp.is(/a/));
+      assert.ok(t.RegExp.is(new RegExp('a')));
+    });
+
+    it('should return false when x is not a regexp', function () {
+      ko(t.RegExp.is(null));
+      ko(t.RegExp.is(undefined));
+      ko(t.RegExp.is(0));
+      ko(t.RegExp.is(''));
+      ko(t.RegExp.is([]));
+      ko(t.RegExp.is(new String('1'))); // eslint-disable-line
+      ko(t.RegExp.is(new Number(1))); // eslint-disable-line
+      ko(t.RegExp.is(new Boolean())); // eslint-disable-line
+      ko(t.RegExp.is(new Error()));
+      ko(t.RegExp.is(new Date()));
+    });
+
+  });
+
+});
+
+describe('t.Date', function () {
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is a Dat', function () {
+      assert.ok(t.Date.is(new Date()));
+    });
+
+    it('should return false when x is not a Dat', function () {
+      ko(t.Date.is(null));
+      ko(t.Date.is(undefined));
+      ko(t.Date.is(0));
+      ko(t.Date.is(''));
+      ko(t.Date.is([]));
+      ko(t.Date.is(new String('1'))); // eslint-disable-line
+      ko(t.Date.is(new Number(1))); // eslint-disable-line
+      ko(t.Date.is(new Boolean())); // eslint-disable-line
+      ko(t.Date.is(new Error()));
+      ko(t.Date.is(/a/));
+      ko(t.Date.is(new RegExp('a')));
+    });
+
+  });
+
+});
diff --git a/test/dict.js b/test/dict.js
new file mode 100644
index 0000000..afd5359
--- /dev/null
+++ b/test/dict.js
@@ -0,0 +1,103 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+var Point = t.struct({
+  x: t.Number,
+  y: t.Number
+});
+
+describe('t.dict(domain, codomain, [name])', function () {
+
+  describe('combinator', function () {
+
+    it('should throw if used with wrong arguments', function () {
+
+      throwsWithMessage(function () {
+        t.dict();
+      }, '[tcomb] Invalid argument domain undefined supplied to dict(domain, codomain, [name]) combinator (expected a type)');
+
+      throwsWithMessage(function () {
+        t.dict(t.String);
+      }, '[tcomb] Invalid argument codomain undefined supplied to dict(domain, codomain, [name]) combinator (expected a type)');
+
+      throwsWithMessage(function () {
+        t.dict(t.String, Point, 1);
+      }, '[tcomb] Invalid argument name 1 supplied to dict(domain, codomain, [name]) combinator (expected a string)');
+
+    });
+
+  });
+
+  describe('constructor', function () {
+
+    var S = t.struct({}, 'S');
+    var Domain = t.subtype(t.String, function (x) {
+      return x !== 'forbidden';
+    }, 'Domain');
+    var T = t.dict(Domain, S, 'T');
+
+    it('should coerce values', function () {
+      var t = T({a: {}});
+      assert.ok(S.is(t.a));
+    });
+
+    it('should accept only valid values', function () {
+
+      throwsWithMessage(function () {
+        T(1);
+      }, '[tcomb] Invalid value 1 supplied to T');
+
+      throwsWithMessage(function () {
+        T({a: 1});
+      }, '[tcomb] Invalid value 1 supplied to T/a: S (expected an object)');
+
+      throwsWithMessage(function () {
+        T({forbidden: {}});
+      }, '[tcomb] Invalid value "forbidden" supplied to T/Domain');
+
+    });
+
+    it('should be idempotent', function () {
+      var T = t.dict(t.String, t.String);
+      var p1 = T({a: 'a', b: 'b'});
+      var p2 = T(p1);
+      assert.deepEqual(Object.isFrozen(p1), true);
+      assert.deepEqual(Object.isFrozen(p2), true);
+      assert.deepEqual(p2 === p1, true);
+    });
+
+  });
+
+  describe('#is(x)', function () {
+
+    var T = t.dict(t.String, Point);
+    var p1 = new Point({x: 0, y: 0});
+    var p2 = new Point({x: 1, y: 1});
+
+    it('should return true when x is a list', function () {
+      assert.ok(T.is({a: p1, b: p2}));
+    });
+
+    it('should not depend on `this`', function () {
+      assert.ok([{a: p1, b: p2}].every(T.is));
+    });
+
+  });
+
+  describe('#update()', function () {
+
+    var Type = t.dict(t.String, t.String);
+    var instance = Type({p1: 'a', p2: 'b'});
+
+    it('should return a new instance', function () {
+      var newInstance = Type.update(instance, {p2: {$set: 'c'}});
+      assert.ok(Type.is(newInstance));
+      assert.deepEqual(instance.p2, 'b');
+      assert.deepEqual(newInstance.p2, 'c');
+    });
+
+  });
+
+});
diff --git a/test/enums.js b/test/enums.js
new file mode 100644
index 0000000..259d6d5
--- /dev/null
+++ b/test/enums.js
@@ -0,0 +1,88 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+describe('t.enums(map, [name])', function () {
+
+  describe('combinator', function () {
+
+    it('should throw if used with wrong arguments', function () {
+
+      throwsWithMessage(function () {
+        t.enums();
+      }, '[tcomb] Invalid argument map undefined supplied to enums(map, [name]) combinator (expected a dictionary of String -> String | Number)');
+
+      throwsWithMessage(function () {
+        t.enums({}, 1);
+      }, '[tcomb] Invalid argument name 1 supplied to enums(map, [name]) combinator (expected a string)');
+
+    });
+
+  });
+
+  describe('constructor', function () {
+
+    var T = t.enums({a: 0}, 'T');
+
+    it('should throw if used with new', function () {
+      throwsWithMessage(function () {
+        var x = new T('a'); // eslint-disable-line
+      }, '[tcomb] Cannot use the new operator to instantiate the type T');
+    });
+
+    it('should accept only valid values', function () {
+      assert.deepEqual(T('a'), 'a');
+      throwsWithMessage(function () {
+        T('b');
+      }, '[tcomb] Invalid value "b" supplied to T (expected one of [\n  "a"\n])');
+    });
+
+  });
+
+  describe('#is(x)', function () {
+
+    var Direction = t.enums({
+      North: 0,
+      East: 1,
+      South: 2,
+      West: 3,
+      1: 'North-East',
+      2.5: 'South-East'
+    });
+
+    it('should return true when x is an instance of the enum', function () {
+      assert.ok(Direction.is('North'));
+      assert.ok(Direction.is(1));
+      assert.ok(Direction.is('1'));
+      assert.ok(Direction.is(2.5));
+    });
+
+    it('should return false when x is not an instance of the enum', function () {
+      assert.strictEqual(Direction.is('North-East'), false);
+      assert.strictEqual(Direction.is(2), false);
+    });
+
+  });
+
+  describe('#of(keys)', function () {
+
+    it('should return an enum', function () {
+      var Size = t.enums.of(['large', 'small', 1, 10.9]);
+      assert.ok(Size.meta.map.large === 'large');
+      assert.ok(Size.meta.map.small === 'small');
+      assert.ok(Size.meta.map['1'] === 1);
+      assert.ok(Size.meta.map[10.9] === 10.9);
+    });
+
+    it('should handle a string', function () {
+      var Size = t.enums.of('large small 10');
+      assert.ok(Size.meta.map.large === 'large');
+      assert.ok(Size.meta.map.small === 'small');
+      assert.ok(Size.meta.map['10'] === '10');
+      assert.ok(Size.meta.map[10] === '10');
+    });
+
+  });
+
+});
diff --git a/test/es6.js b/test/es6.js
new file mode 100644
index 0000000..ef037f8
--- /dev/null
+++ b/test/es6.js
@@ -0,0 +1,86 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+describe('ES6 classes', function () {
+
+  function Class(a) {
+    this.a = a;
+  }
+
+  var c = new Class('a');
+
+  it('should be handled by subtype', function () {
+    var T = t.subtype(Class, function isA(x) {
+      return x.a === 'a';
+    });
+    assert.deepEqual(T.is(c), true);
+    throwsWithMessage(function () {
+      T(new Class('b'));
+    }, '[tcomb] Invalid value {\n  "a": "b"\n} supplied to {Class | isA}');
+  });
+
+  it('should be handled by struct', function () {
+    var T = t.struct({
+      c: Class
+    }, 'T');
+    assert.deepEqual(T.is(new T({c: c})), true);
+    throwsWithMessage(function () {
+      T({c: 1});
+    }, '[tcomb] Invalid value 1 supplied to T/c: Class');
+  });
+
+  it('should be handled by maybe', function () {
+    var T = t.maybe(Class);
+    assert.deepEqual(T.is(null), true);
+    assert.deepEqual(T.is(c), true);
+    throwsWithMessage(function () {
+      T(1);
+    }, '[tcomb] Invalid value 1 supplied to Class');
+  });
+
+  it('should be handled by tuple', function () {
+    var T = t.tuple([Class]);
+    assert.deepEqual(T.is([c]), true);
+    throwsWithMessage(function () {
+      T([1]);
+    }, '[tcomb] Invalid value 1 supplied to [Class]/0: Class');
+  });
+
+  it('should be handled by list', function () {
+    var T = t.list(Class);
+    assert.deepEqual(T.is([c]), true);
+    throwsWithMessage(function () {
+      T([1]);
+    }, '[tcomb] Invalid value 1 supplied to Array<Class>/0: Class');
+  });
+
+  it('should be handled by dict', function () {
+    var T = t.dict(t.String, Class);
+    assert.deepEqual(T.is({a: c}), true);
+    throwsWithMessage(function () {
+      T({a: 1});
+    }, '[tcomb] Invalid value 1 supplied to {[key: String]: Class}/a: Class');
+  });
+
+  it('should be handled by union', function () {
+    var T = t.union([t.String, Class]);
+    assert.deepEqual(T.is(c), true);
+    throwsWithMessage(function () {
+      T(1);
+    }, '[tcomb] Invalid value 1 supplied to String | Class');
+  });
+
+  it('should be handled by func', function () {
+    var T = t.func(Class, t.String);
+    var f = T.of(function (c) {
+      return c.constructor.name;
+    });
+    assert.deepEqual(f(c), 'Class');
+    throwsWithMessage(function () {
+      f(1);
+    }, '[tcomb] Invalid value 1 supplied to [Class]/0: Class');
+  });
+
+});
diff --git a/test/func.js b/test/func.js
new file mode 100644
index 0000000..d413972
--- /dev/null
+++ b/test/func.js
@@ -0,0 +1,146 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+describe('t.func(domain, codomain, [name])', function () {
+
+  it('should handle a no types', function () {
+    var T = t.func([], t.String);
+    assert.deepEqual(T.meta.domain.length, 0);
+    var getGreeting = T.of(function () { return 'Hi'; });
+    assert.deepEqual(getGreeting(), 'Hi');
+  });
+
+  it('should handle a single type', function () {
+    var T = t.func(t.Number, t.Number);
+    assert.deepEqual(T.meta.domain.length, 1);
+    assert.ok(T.meta.domain[0] === t.Number);
+  });
+
+  it('should automatically instrument a function', function () {
+    var T = t.func(t.Number, t.Number);
+    var f = function () { return 'hi'; };
+    assert.ok(T.is(T(f)));
+  });
+
+  describe('of', function () {
+
+    it('should check the arguments', function () {
+
+      var T = t.func([t.Number, t.Number], t.Number);
+      var sum = T.of(function (a, b) {
+        return a + b;
+      });
+      assert.deepEqual(sum(1, 2), 3);
+
+      throwsWithMessage(function () {
+        sum(1, 2, 3);
+      }, '[tcomb] Invalid value [\n  1,\n  2,\n  3\n] supplied to [Number, Number] (expected an array of length 2)');
+
+      throwsWithMessage(function () {
+        sum('a', 2);
+      }, '[tcomb] Invalid value "a" supplied to [Number, Number]/0: Number');
+
+    });
+
+    it('should check the return value', function () {
+
+      var T = t.func([t.Number, t.Number], t.Number);
+      var sum = T.of(function () {
+        return 'a';
+      });
+
+      throwsWithMessage(function () {
+        sum(1, 2);
+      }, '[tcomb] Invalid value "a" supplied to Number');
+
+    });
+
+    it('should preserve `this`', function () {
+      var o = {name: 'giulio'};
+      o.getTypeName = t.func([], t.String).of(function () {
+        return this.name;
+      });
+      assert.deepEqual(o.getTypeName(), 'giulio');
+    });
+
+    it('should handle function types', function () {
+      var A = t.func([t.String], t.String);
+      var B = t.func([t.String, A], t.String);
+
+      var f = A.of(function (s) {
+        return s + '!';
+      });
+      var g = B.of(function (str, strAction) {
+        return strAction(str);
+      });
+
+      assert.deepEqual(g('hello', f), 'hello!');
+    });
+
+    it('should be idempotent', function () {
+      var f = function (s) { return s; };
+      var g = t.func([t.String], t.String).of(f);
+      var h = t.func([t.String], t.String).of(g);
+      assert.ok(h === g);
+    });
+
+  });
+
+  describe('currying', function () {
+
+    it('should curry functions', function () {
+      var Type = t.func([t.Number, t.Number, t.Number], t.Number);
+      var sum = Type.of(function (a, b, c) {
+        return a + b + c;
+      }, true);
+      assert.deepEqual(sum(1, 2, 3), 6);
+      assert.deepEqual(sum(1, 2)(3), 6);
+      assert.deepEqual(sum(1)(2, 3), 6);
+      assert.deepEqual(sum(1)(2)(3), 6);
+
+      // important: the curried function must be of the correct type
+      var CurriedType = t.func([t.Number, t.Number], t.Number);
+      var sum1 = sum(1);
+      assert.deepEqual(sum1(2, 3), 6);
+      assert.deepEqual(sum1(2)(3), 6);
+      assert.ok(CurriedType.is(sum1));
+    });
+
+    it('should throw if partial arguments are wrong', function () {
+
+      var T = t.func([t.Number, t.Number], t.Number);
+      var sum = T.of(function (a, b) {
+        return a + b;
+      }, true);
+
+      throwsWithMessage(function () {
+        sum('a');
+      }, '[tcomb] Invalid value "a" supplied to [Number]/0: Number');
+
+      throwsWithMessage(function () {
+        var sum1 = sum(1);
+        sum1('a');
+      }, '[tcomb] Invalid value "a" supplied to [Number]/0: Number');
+
+    });
+
+  });
+
+  describe('uncurried', function () {
+
+    it('should not curry functions', function () {
+      var Type = t.func([t.Number, t.Number, t.Number], t.Number);
+      var sum = Type.of(function (a, b, c) {
+        return a + b + c;
+      });
+      assert.deepEqual(sum(1, 2, 3), 6);
+      throwsWithMessage(function () {
+        sum(1, 2);
+      }, '[tcomb] Invalid value [\n  1,\n  2\n] supplied to [Number, Number, Number] (expected an array of length 3)');
+    });
+
+  });
+
+});
diff --git a/test/getTypeName.js b/test/getTypeName.js
new file mode 100644
index 0000000..c28c790
--- /dev/null
+++ b/test/getTypeName.js
@@ -0,0 +1,58 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+
+describe('t.getTypeName(type)', function () {
+
+  var UnnamedStruct = t.struct({});
+  var NamedStruct = t.struct({}, 'NamedStruct');
+  var UnnamedUnion = t.union([t.String, t.Number]);
+  var NamedUnion = t.union([t.String, t.Number], 'NamedUnion');
+  var UnnamedMaybe = t.maybe(t.String);
+  var NamedMaybe = t.maybe(t.String, 'NamedMaybe');
+  var UnnamedEnums = t.enums({a: 'A', b: 'B'});
+  var NamedEnums = t.enums({}, 'NamedEnums');
+  var UnnamedTuple = t.tuple([t.String, t.Number]);
+  var NamedTuple = t.tuple([t.String, t.Number], 'NamedTuple');
+  var UnnamedSubtype = t.subtype(t.String, function notEmpty(x) { return x !== ''; });
+  var NamedSubtype = t.subtype(t.String, function (x) { return x !== ''; }, 'NamedSubtype');
+  var UnnamedList = t.list(t.String);
+  var NamedList = t.list(t.String, 'NamedList');
+  var UnnamedDict = t.dict(t.String, t.String);
+  var NamedDict = t.dict(t.String, t.String, 'NamedDict');
+  var UnnamedFunc = t.func(t.String, t.String);
+  var NamedFunc = t.func(t.String, t.String, 'NamedFunc');
+  var UnnamedIntersection = t.intersection([t.String, t.Number]);
+  var NamedIntersection = t.intersection([t.String, t.Number], 'NamedIntersection');
+
+  it('should return the name of a function', function () {
+    assert.deepEqual(t.getTypeName(function myname(){}), 'myname');
+  });
+
+  it('should return the name of a named type', function () {
+    assert.deepEqual(t.getTypeName(NamedStruct), 'NamedStruct');
+    assert.deepEqual(t.getTypeName(NamedUnion), 'NamedUnion');
+    assert.deepEqual(t.getTypeName(NamedMaybe), 'NamedMaybe');
+    assert.deepEqual(t.getTypeName(NamedEnums), 'NamedEnums');
+    assert.deepEqual(t.getTypeName(NamedTuple), 'NamedTuple');
+    assert.deepEqual(t.getTypeName(NamedSubtype), 'NamedSubtype');
+    assert.deepEqual(t.getTypeName(NamedList), 'NamedList');
+    assert.deepEqual(t.getTypeName(NamedDict), 'NamedDict');
+    assert.deepEqual(t.getTypeName(NamedFunc), 'NamedFunc');
+    assert.deepEqual(t.getTypeName(NamedIntersection), 'NamedIntersection');
+  });
+
+  it('should return a meaningful name of a unnamed type', function () {
+    assert.deepEqual(t.getTypeName(UnnamedStruct), '{}');
+    assert.deepEqual(t.getTypeName(UnnamedUnion), 'String | Number');
+    assert.deepEqual(t.getTypeName(UnnamedMaybe), '?String');
+    assert.deepEqual(t.getTypeName(UnnamedEnums), '"a" | "b"');
+    assert.deepEqual(t.getTypeName(UnnamedTuple), '[String, Number]');
+    assert.deepEqual(t.getTypeName(UnnamedSubtype), '{String | notEmpty}');
+    assert.deepEqual(t.getTypeName(UnnamedList), 'Array<String>');
+    assert.deepEqual(t.getTypeName(UnnamedDict), '{[key: String]: String}');
+    assert.deepEqual(t.getTypeName(UnnamedFunc), '(String) => String');
+    assert.deepEqual(t.getTypeName(UnnamedIntersection), 'String & Number');
+  });
+
+});
diff --git a/test/intersection.js b/test/intersection.js
new file mode 100644
index 0000000..e2e0a12
--- /dev/null
+++ b/test/intersection.js
@@ -0,0 +1,56 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+describe('t.intersection(types, [name])', function () {
+
+  var Min = t.subtype(t.String, function (s) { return s.length > 2; }, 'Min');
+  var Max = t.subtype(t.String, function (s) { return s.length < 5; }, 'Max');
+  var MinMax = t.intersection([Min, Max], 'MinMax');
+
+  describe('combinator', function () {
+
+    it('should throw if used with wrong arguments', function () {
+
+      throwsWithMessage(function () {
+        t.intersection();
+      }, '[tcomb] Invalid argument types undefined supplied to intersection(types, [name]) combinator (expected an array of at least 2 types)');
+
+      throwsWithMessage(function () {
+        t.intersection([]);
+      }, '[tcomb] Invalid argument types [] supplied to intersection(types, [name]) combinator (expected an array of at least 2 types)');
+
+      throwsWithMessage(function () {
+        t.intersection([1]);
+      }, '[tcomb] Invalid argument types [\n  1\n] supplied to intersection(types, [name]) combinator (expected an array of at least 2 types)');
+
+      throwsWithMessage(function () {
+        t.intersection([Min, Max], 1);
+      }, '[tcomb] Invalid argument name 1 supplied to intersection(types, [name]) combinator (expected a string)');
+
+    });
+
+  });
+
+  describe('constructor', function () {
+
+    it('should throw if used with wrong arguments', function () {
+      throwsWithMessage(function () {
+        MinMax('a')
+      }, '[tcomb] Invalid value "a" supplied to MinMax');
+    });
+
+  });
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is an instance of the intersection', function () {
+      assert.ok(MinMax.is('123'), '3 chars');
+      assert.strictEqual(MinMax.is('12'), false);
+      assert.strictEqual(MinMax.is('12345'), false);
+    });
+
+  });
+
+});
diff --git a/test/list.js b/test/list.js
new file mode 100644
index 0000000..67243c4
--- /dev/null
+++ b/test/list.js
@@ -0,0 +1,92 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+var Point = t.struct({
+  x: t.Number,
+  y: t.Number
+});
+
+describe('t.list(type, [name])', function () {
+
+  describe('combinator', function () {
+
+    it('should throw if used with wrong arguments', function () {
+
+      throwsWithMessage(function () {
+        t.list();
+      }, '[tcomb] Invalid argument type undefined supplied to list(type, [name]) combinator (expected a type)');
+
+      throwsWithMessage(function () {
+        t.list(Point, 1);
+      }, '[tcomb] Invalid argument name 1 supplied to list(type, [name]) combinator (expected a string)');
+
+    });
+
+  });
+
+  describe('constructor', function () {
+
+    var S = t.struct({}, 'S');
+    var T = t.list(S, 'T');
+
+    it('should coerce values', function () {
+      var t = T([{}]);
+      assert.ok(S.is(t[0]));
+    });
+
+    it('should accept only valid values', function () {
+
+      throwsWithMessage(function () {
+        T(1);
+      }, '[tcomb] Invalid value 1 supplied to T (expected an array of S)');
+
+      throwsWithMessage(function () {
+        T([1]);
+      }, '[tcomb] Invalid value 1 supplied to T/0: S (expected an object)');
+
+    });
+
+    it('should be idempotent', function () {
+      var T = t.list(t.Number);
+      var p1 = T([1, 2]);
+      var p2 = T(p1);
+      assert.deepEqual(Object.isFrozen(p1), true);
+      assert.deepEqual(Object.isFrozen(p2), true);
+      assert.deepEqual(p2 === p1, true);
+    });
+
+  });
+
+  describe('#is(x)', function () {
+
+    var Path = t.list(Point);
+    var p1 = new Point({x: 0, y: 0});
+    var p2 = new Point({x: 1, y: 1});
+
+    it('should return true when x is a list', function () {
+      assert.ok(Path.is([p1, p2]));
+    });
+
+    it('should not depend on `this`', function () {
+      assert.ok([[p1, p2]].every(Path.is));
+    });
+
+  });
+
+  describe('#update()', function () {
+
+    var Type = t.list(t.String);
+    var instance = Type(['a', 'b']);
+
+    it('should return a new instance', function () {
+      var newInstance = Type.update(instance, {'$push': ['c']});
+      assert(Type.is(newInstance));
+      assert(instance.length === 2);
+      assert(newInstance.length === 3);
+    });
+
+  });
+
+});
diff --git a/test/match.js b/test/match.js
new file mode 100644
index 0000000..0b79b0e
--- /dev/null
+++ b/test/match.js
@@ -0,0 +1,62 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+describe('match', function () {
+
+  it('should match on type constructors', function () {
+    assert.deepEqual(t.match(1,
+      t.String, function () { return 'a string'; },
+      t.Number, function (n) { return 2 * n; }
+    ), 2);
+  });
+
+  it('should handle an optional guard', function () {
+    assert.deepEqual(t.match(1,
+      t.String, function () { return 'a string'; },
+      t.Number, function () { return false; }, function (n) { return 2 * n; },
+      t.Number, function (n) { return 3 * n; }
+    ), 3);
+  });
+
+  it('should throw if no match is found', function () {
+    throwsWithMessage(function () {
+      t.match(true,
+        t.String, function () { return 'a string'; },
+        t.Number, function (n) { return 2 * n; }
+      );
+    }, '[tcomb] Match error');
+  });
+
+  it('should throw if cases are misplaced', function () {
+    throwsWithMessage(function () {
+      t.match(true,
+        t.String, function () { return 'a string'; },
+        t.Number
+      );
+    }, '[tcomb] Invalid block in clause #2');
+  });
+
+  it('should handle unions of unions', function () {
+    var A = t.subtype(t.String, function (s) { return s === 'A'; });
+    var B = t.subtype(t.String, function (s) { return s === 'B'; });
+    var C = t.subtype(t.String, function (s) { return s === 'C'; });
+    var D = t.subtype(t.String, function (s) { return s === 'D'; });
+    var E = t.subtype(t.String, function (s) { return s === 'E'; });
+    var F = t.subtype(t.String, function (s) { return s === 'F'; });
+    var G = t.subtype(t.String, function (s) { return s === 'G'; });
+    var H = t.subtype(t.String, function (s) { return s === 'H'; });
+    var U1 = t.union([A, B]);
+    var U2 = t.union([C, D]);
+    var U3 = t.union([E, F]);
+    var U4 = t.union([G, H]);
+    var UU1 = t.union([U1, U2]);
+    var UU2 = t.union([U3, U4]);
+    assert.deepEqual(t.match('F',
+      UU1, function () { return '1'; },
+      UU2, function () { return '2'; }
+    ), '2');
+  });
+
+});
diff --git a/test/maybe.js b/test/maybe.js
new file mode 100644
index 0000000..e433134
--- /dev/null
+++ b/test/maybe.js
@@ -0,0 +1,80 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+var Point = t.struct({
+  x: t.Number,
+  y: t.Number
+});
+
+describe('t.maybe(type, [name])', function () {
+
+  describe('combinator', function () {
+
+    it('should throw if used with wrong arguments', function () {
+
+      throwsWithMessage(function () {
+        t.maybe();
+      }, '[tcomb] Invalid argument type undefined supplied to maybe(type, [name]) combinator (expected a type)');
+
+      throwsWithMessage(function () {
+        t.maybe(Point, 1);
+      }, '[tcomb] Invalid argument name 1 supplied to maybe(type, [name]) combinator (expected a string)');
+
+    });
+
+    it('should be idempotent', function () {
+      var MaybeStr = t.maybe(t.String);
+      assert.ok(t.maybe(MaybeStr) === MaybeStr);
+    });
+
+    it('should be noop with Any', function () {
+      assert.ok(t.maybe(t.Any) === t.Any);
+    });
+
+    it('should be noop with Nil', function () {
+      assert.ok(t.maybe(t.Nil) === t.Nil);
+    });
+
+  });
+
+  describe('constructor', function () {
+
+    it('should throw if used with new', function () {
+      throwsWithMessage(function () {
+        var T = t.maybe(t.String, 'T');
+        var x = new T(); // eslint-disable-line
+      }, '[tcomb] Cannot use the new operator to instantiate the type T');
+    });
+
+    it('should coerce values', function () {
+      var T = t.maybe(Point);
+      assert.deepEqual(T(null), null);
+      assert.deepEqual(T(undefined), null);
+      assert.ok(Point.is(T({x: 0, y: 0})));
+    });
+
+    it('should be idempotent', function () {
+      var T = t.maybe(Point);
+      var p1 = T({x: 0, y: 0});
+      var p2 = T(p1);
+      assert.deepEqual(Object.isFrozen(p1), true);
+      assert.deepEqual(Object.isFrozen(p2), true);
+      assert.deepEqual(p2 === p1, true);
+    });
+
+  });
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is an instance of the maybe', function () {
+      var Radio = t.maybe(t.String);
+      assert.ok(Radio.is('a'));
+      assert.ok(Radio.is(null));
+      assert.ok(Radio.is(undefined));
+    });
+
+  });
+
+});
diff --git a/test/mixin.js b/test/mixin.js
new file mode 100644
index 0000000..d58e825
--- /dev/null
+++ b/test/mixin.js
@@ -0,0 +1,42 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+describe('t.mixin(x, y, [overwrite])', function () {
+
+  it('should mix two objects', function () {
+    var o1 = {a: 1};
+    var o2 = {b: 2};
+    var o3 = t.mixin(o1, o2);
+    assert.strictEqual(o3, o1);
+    assert.deepEqual(o3.a, 1);
+    assert.deepEqual(o3.b, 2);
+  });
+
+  it('should throw if a property already exists', function () {
+    throwsWithMessage(function () {
+      var o1 = {a: 1};
+      var o2 = {a: 2, b: 2};
+      t.mixin(o1, o2);
+    }, '[tcomb] Invalid call to mixin(target, source, [overwrite]): cannot overwrite property "a" of target object');
+  });
+
+  it('should not throw if a property already exists but overwrite = true', function () {
+    var o1 = {a: 1};
+    var o2 = {a: 2, b: 2};
+    var o3 = t.mixin(o1, o2, true);
+    assert.deepEqual(o3.a, 2);
+    assert.deepEqual(o3.b, 2);
+  });
+
+  it('should not mix prototype properties', function () {
+    function F() {}
+    F.prototype.method = function () {};
+    var source = new F();
+    var target = {};
+    t.mixin(target, source);
+    assert.deepEqual(target.method, undefined);
+  });
+
+});
diff --git a/test/struct.js b/test/struct.js
new file mode 100644
index 0000000..1fc4c2b
--- /dev/null
+++ b/test/struct.js
@@ -0,0 +1,143 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+var Point = t.struct({
+  x: t.Number,
+  y: t.Number
+});
+
+describe('t.struct(props, [name])', function () {
+
+  describe('combinator', function () {
+
+    it('should throw if used with wrong arguments', function () {
+
+      throwsWithMessage(function () {
+        t.struct();
+      }, '[tcomb] Invalid argument props undefined supplied to struct(props, [name]) combinator (expected a dictionary String -> Type)');
+
+      throwsWithMessage(function () {
+        t.struct({a: null});
+      }, '[tcomb] Invalid argument props {\n  "a": null\n} supplied to struct(props, [name]) combinator (expected a dictionary String -> Type)');
+
+      throwsWithMessage(function () {
+        t.struct({}, 1);
+      }, '[tcomb] Invalid argument name 1 supplied to struct(props, [name]) combinator (expected a string)');
+
+    });
+
+  });
+
+  describe('constructor', function () {
+
+    it('should be idempotent', function () {
+      var T = Point;
+      var p1 = T({x: 0, y: 0});
+      var p2 = T(p1);
+      assert.deepEqual(Object.isFrozen(p1), true);
+      assert.deepEqual(Object.isFrozen(p2), true);
+      assert.deepEqual(p2 === p1, true);
+    });
+
+    it('should accept only valid values', function () {
+      throwsWithMessage(function () {
+        Point(1);
+      }, '[tcomb] Invalid value 1 supplied to {x: Number, y: Number} (expected an object)');
+      throwsWithMessage(function () {
+        Point({});
+      }, '[tcomb] Invalid value undefined supplied to {x: Number, y: Number}/x: Number');
+    });
+
+  });
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is an instance of the struct', function () {
+      var p = new Point({ x: 1, y: 2 });
+      assert.ok(Point.is(p));
+    });
+
+  });
+
+  describe('#update()', function () {
+
+    var Type = t.struct({name: t.String});
+    var instance = new Type({name: 'Giulio'});
+
+    it('should return a new instance', function () {
+      var newInstance = Type.update(instance, {name: {$set: 'Canti'}});
+      assert.ok(Type.is(newInstance));
+      assert.deepEqual(instance.name, 'Giulio');
+      assert.deepEqual(newInstance.name, 'Canti');
+    });
+
+  });
+
+  describe('#extend(props, [name])', function () {
+
+    it('should extend an existing struct', function () {
+      var Point = t.struct({
+        x: t.Number,
+        y: t.Number
+      }, 'Point');
+      var Point3D = Point.extend({z: t.Number}, 'Point3D');
+      assert.deepEqual(t.getTypeName(Point3D), 'Point3D');
+      assert.deepEqual(Point3D.meta.props.x, t.Number);
+      assert.deepEqual(Point3D.meta.props.y, t.Number);
+      assert.deepEqual(Point3D.meta.props.z, t.Number);
+    });
+
+    it('should handle an array as argument', function () {
+      var Type = t.struct({a: t.String}, 'Type');
+      var Mixin = [{b: t.Number, c: t.Boolean}];
+      var NewType = Type.extend(Mixin, 'NewType');
+      assert.deepEqual(t.getTypeName(NewType), 'NewType');
+      assert.deepEqual(NewType.meta.props.a, t.String);
+      assert.deepEqual(NewType.meta.props.b, t.Number);
+      assert.deepEqual(NewType.meta.props.c, t.Boolean);
+    });
+
+    it('should handle a struct (or list of structs) as argument', function () {
+      var A = t.struct({a: t.String}, 'A');
+      var B = t.struct({b: t.String}, 'B');
+      var C = t.struct({c: t.String}, 'C');
+      var MixinD = {d: t.String};
+      var E = A.extend([B, C, MixinD]);
+      assert.deepEqual(E.meta.props, {
+        a: t.String,
+        b: t.String,
+        c: t.String,
+        d: t.String
+      });
+    });
+
+    it('should support prototypal inheritance', function () {
+      var Rectangle = t.struct({
+        w: t.Number,
+        h: t.Number
+      }, 'Rectangle');
+      Rectangle.prototype.area = function () {
+        return this.w * this.h;
+      };
+      var Cube = Rectangle.extend({
+        l: t.Number
+      });
+      Cube.prototype.volume = function () {
+        return this.area() * this.l;
+      };
+
+      assert(typeof Rectangle.prototype.area === 'function');
+      assert(typeof Cube.prototype.area === 'function');
+      assert(undefined === Rectangle.prototype.volume);
+      assert(typeof Cube.prototype.volume === 'function');
+      assert(Cube.prototype.constructor === Cube);
+
+      var c = new Cube({w: 2, h: 2, l: 2});
+      assert.deepEqual(c.volume(), 8);
+    });
+
+  });
+
+});
diff --git a/test/subtype.js b/test/subtype.js
new file mode 100644
index 0000000..d23d534
--- /dev/null
+++ b/test/subtype.js
@@ -0,0 +1,89 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+var Point = t.struct({
+  x: t.Number,
+  y: t.Number
+});
+
+describe('t.subtype(type, predicate, [name])', function () {
+
+  var True = function () { return true; };
+
+  describe('combinator', function () {
+
+    it('should throw if used with wrong arguments', function () {
+
+      throwsWithMessage(function () {
+        t.subtype();
+      }, '[tcomb] Invalid argument type undefined supplied to subtype(type, predicate, [name]) combinator (expected a type)');
+
+      throwsWithMessage(function () {
+        t.subtype(Point, null);
+      }, '[tcomb] Invalid argument predicate supplied to subtype(type, predicate, [name]) combinator (expected a function)');
+
+      throwsWithMessage(function () {
+        t.subtype(Point, True, 1);
+      }, '[tcomb] Invalid argument name 1 supplied to subtype(type, predicate, [name]) combinator (expected a string)');
+
+    });
+
+  });
+
+  describe('constructor', function () {
+
+    it('should throw if used with new and a type that is not instantiable with new', function () {
+      throwsWithMessage(function () {
+        var T = t.subtype(t.String, function () { return true; }, 'T');
+        var x = new T(); // eslint-disable-line
+      }, '[tcomb] Cannot use the new operator to instantiate the type T');
+    });
+
+    it('should coerce values', function () {
+      var T = t.subtype(Point, function () { return true; });
+      var p = T({x: 0, y: 0});
+      assert.ok(Point.is(p));
+    });
+
+    it('should accept only valid values', function () {
+      var predicate = function (p) { return p.x > 0; };
+      var T = t.subtype(Point, predicate, 'T');
+      throwsWithMessage(function () {
+        T({x: 0, y: 0});
+      }, '[tcomb] Invalid value {\n  "x": 0,\n  "y": 0\n} supplied to T');
+    });
+
+  });
+
+  describe('#is(x)', function () {
+
+    var Positive = t.subtype(t.Number, function (n) {
+      return n >= 0;
+    });
+
+    it('should return true when x is a subtype', function () {
+      assert.ok(Positive.is(1));
+    });
+
+    it('should return false when x is not a subtype', function () {
+      assert.strictEqual(Positive.is(-1), false);
+    });
+
+  });
+
+  describe('#update()', function () {
+
+    var Type = t.subtype(t.String, function (s) { return s.length > 2; });
+    var instance = Type('abc');
+
+    it('should return a new instance', function () {
+      var newInstance = Type.update(instance, {'$set': 'bca'});
+      assert(Type.is(newInstance));
+      assert.deepEqual(newInstance, 'bca');
+    });
+
+  });
+
+});
diff --git a/test/test.js b/test/test.js
deleted file mode 100644
index 737c3db..0000000
--- a/test/test.js
+++ /dev/null
@@ -1,1901 +0,0 @@
-/* globals describe, it */
-'use strict';
-var assert = require('assert');
-var t = require('../index');
-
-//
-// setup
-//
-
-var ok = function (x, message) { assert.strictEqual(x, true, message); };
-var ko = function (x, message) { assert.strictEqual(x, false, message); };
-var eq = assert.deepEqual;
-var throwsWithMessage = function (f, message) {
-  assert.throws(f, function (err) {
-    ok(err instanceof Error);
-    assert.strictEqual(err.message, message);
-    return true;
-  });
-};
-var doesNotThrow = assert.doesNotThrow;
-
-var noop = function () {};
-var Point = t.struct({
-  x: t.Number,
-  y: t.Number
-});
-
-describe('t.update(instance, spec)', function () {
-
-  var update = t.update;
-  var Tuple = t.tuple([t.String, t.Number]);
-  var List = t.list(t.Number);
-  var Dict = t.dict(t.String, t.Number);
-
-  it('should throw if spec is invalid', function () {
-    throwsWithMessage(function () {
-      t.update({});
-    }, '[tcomb] Invalid argument spec = undefined supplied to function update(instance, spec): expected an object containing commands');
-  });
-
-  it('should handle $set command', function () {
-    var instance = 1;
-    var actual = update(instance, {$set: 2});
-    eq(actual, 2);
-    instance = [1, 2, 3];
-    actual = update(instance, {1: {'$set': 4}});
-    eq(instance, [1, 2, 3]);
-    eq(actual, [1, 4, 3]);
-  });
-
-  it('$set and null value, fix #65', function () {
-    var NullStruct = t.struct({a: t.Number, b: t.maybe(t.Number)});
-    var instance = new NullStruct({a: 1});
-    var updated = update(instance, {b: {$set: 2}});
-    eq(instance, {a: 1, b: null});
-    eq(updated, {a: 1, b: 2});
-  });
-
-  it('should handle $apply command', function () {
-    var $apply = function (n) { return n + 1; };
-    var instance = 1;
-    var actual = update(instance, {$apply: $apply});
-    eq(actual, 2);
-    instance = [1, 2, 3];
-    actual = update(instance, {1: {'$apply': $apply}});
-    eq(instance, [1, 2, 3]);
-    eq(actual, [1, 3, 3]);
-  });
-
-  it('should handle $unshift command', function () {
-    var actual = update([1, 2, 3], {'$unshift': [4]});
-    eq(actual, [4, 1, 2, 3]);
-    actual = update([1, 2, 3], {'$unshift': [4, 5]});
-    eq(actual, [4, 5, 1, 2, 3]);
-    actual = update([1, 2, 3], {'$unshift': [[4, 5]]});
-    eq(actual, [[4, 5], 1, 2, 3]);
-  });
-
-  it('should handle $push command', function () {
-    var actual = update([1, 2, 3], {'$push': [4]});
-    eq(actual, [1, 2, 3, 4]);
-    actual = update([1, 2, 3], {'$push': [4, 5]});
-    eq(actual, [1, 2, 3, 4, 5]);
-    actual = update([1, 2, 3], {'$push': [[4, 5]]});
-    eq(actual, [1, 2, 3, [4, 5]]);
-  });
-
-  it('should handle $splice command', function () {
-    var instance = [1, 2, {a: [12, 17, 15]}];
-    var actual = update(instance, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
-    eq(instance, [1, 2, {a: [12, 17, 15]}]);
-    eq(actual, [1, 2, {a: [12, 13, 14, 15]}]);
-  });
-
-  it('should handle $remove command', function () {
-    var instance = {a: 1, b: 2};
-    var actual = update(instance, {'$remove': ['a']});
-    eq(instance, {a: 1, b: 2});
-    eq(actual, {b: 2});
-  });
-
-  it('should handle $swap command', function () {
-    var instance = [1, 2, 3, 4];
-    var actual = update(instance, {'$swap': {from: 1, to: 2}});
-    eq(instance, [1, 2, 3, 4]);
-    eq(actual, [1, 3, 2, 4]);
-  });
-
-  describe('structs', function () {
-
-    var instance = new Point({x: 0, y: 1});
-
-    it('should handle $set command', function () {
-      var updated = update(instance, {x: {$set: 1}});
-      eq(instance, {x: 0, y: 1});
-      eq(updated, {x: 1, y: 1});
-    });
-
-    it('should handle $apply command', function () {
-      var updated = update(instance, {x: {$apply: function (x) {
-        return x + 2;
-      }}});
-      eq(instance, {x: 0, y: 1});
-      eq(updated, {x: 2, y: 1});
-    });
-
-    it('should handle $merge command', function () {
-      var updated = update(instance, {'$merge': {x: 2, y: 2}});
-      eq(instance, {x: 0, y: 1});
-      eq(updated, {x: 2, y: 2});
-      var Nested = t.struct({
-        a: t.Number,
-        b: t.struct({
-          c: t.Number,
-          d: t.Number,
-          e: t.Number
-        })
-      });
-      instance = new Nested({a: 1, b: {c: 2, d: 3, e: 4}});
-      updated = update(instance, {b: {'$merge': {c: 5, e: 6}}});
-      eq(instance, {a: 1, b: {c: 2, d: 3, e: 4}});
-      eq(updated, {a: 1, b: {c: 5, d: 3, e: 6}});
-    });
-
-  });
-
-  describe('tuples', function () {
-
-    var instance = Tuple(['a', 1]);
-
-    it('should handle $set command', function () {
-      var updated = update(instance, {0: {$set: 'b'}});
-      eq(updated, ['b', 1]);
-    });
-
-  });
-
-  describe('lists', function () {
-
-    var instance = List([1, 2, 3, 4]);
-
-    it('should handle $set command', function () {
-      var updated = update(instance, {2: {$set: 5}});
-      eq(updated, [1, 2, 5, 4]);
-    });
-
-    it('should handle $splice command', function () {
-      var updated = update(instance, {$splice: [[1, 2, 5, 6]]});
-      eq(updated, [1, 5, 6, 4]);
-    });
-
-    it('should handle $concat command', function () {
-      var updated = update(instance, {$push: [5]});
-      eq(updated, [1, 2, 3, 4, 5]);
-      updated = update(instance, {$push: [5, 6]});
-      eq(updated, [1, 2, 3, 4, 5, 6]);
-    });
-
-    it('should handle $prepend command', function () {
-      var updated = update(instance, {$unshift: [5]});
-      eq(updated, [5, 1, 2, 3, 4]);
-      updated = update(instance, {$unshift: [5, 6]});
-      eq(updated, [5, 6, 1, 2, 3, 4]);
-    });
-
-    it('should handle $swap command', function () {
-      var updated = update(instance, {$swap: {from: 1, to: 2}});
-      eq(updated, [1, 3, 2, 4]);
-    });
-
-  });
-
-  describe('dicts', function () {
-
-    var instance = Dict({a: 1, b: 2});
-
-    it('should handle $set command', function () {
-      var updated = update(instance, {a: {$set: 2}});
-      eq(updated, {a: 2, b: 2});
-      updated = update(instance, {c: {$set: 3}});
-      eq(updated, {a: 1, b: 2, c: 3});
-    });
-
-    it('should handle $remove command', function () {
-      var updated = update(instance, {$remove: ['a']});
-      eq(updated, {b: 2});
-    });
-
-  });
-
-  describe('memory saving', function () {
-
-    it('should reuse members that are not updated', function () {
-      var Struct = t.struct({
-        a: t.Number,
-        b: t.String,
-        c: t.tuple([t.Number, t.Number]),
-      });
-      var List = t.list(Struct);
-      var instance = List([{
-        a: 1,
-        b: 'one',
-        c: [1000, 1000000]
-      }, {
-        a: 2,
-        b: 'two',
-        c: [2000, 2000000]
-      }]);
-
-      var updated = update(instance, {
-        1: {
-          a: {$set: 119}
-        }
-      });
-
-      assert.strictEqual(updated[0], instance[0]);
-      assert.notStrictEqual(updated[1], instance[1]);
-      assert.strictEqual(updated[1].c, instance[1].c);
-    });
-  });
-
-  describe('all together now', function () {
-
-    it('should handle mixed commands', function () {
-      var Struct = t.struct({
-        a: t.Number,
-        b: Tuple,
-        c: List,
-        d: Dict
-      });
-      var instance = new Struct({
-        a: 1,
-        b: ['a', 1],
-        c: [1, 2, 3, 4],
-        d: {a: 1, b: 2}
-      });
-      var updated = update(instance, {
-        a: {$set: 1},
-        b: {0: {$set: 'b'}},
-        c: {2: {$set: 5}},
-        d: {$remove: ['a']}
-      });
-      eq(updated, {
-        a: 1,
-        b: ['b', 1],
-        c: [1, 2, 5, 4],
-        d: {b: 2}
-      });
-    });
-
-    it('should handle nested structures', function () {
-      var Struct = t.struct({
-        a: t.struct({
-          b: t.tuple([
-            t.String,
-            t.list(t.Number)
-          ])
-        })
-      });
-      var instance = new Struct({
-        a: {
-          b: ['a', [1, 2, 3]]
-        }
-      });
-      var updated = update(instance, {
-        a: {b: {1: {2: {$set: 4}}}}
-      });
-      eq(updated, {
-        a: {
-          b: ['a', [1, 2, 4]]
-        }
-      });
-    });
-
-  });
-
-});
-
-//
-// assert
-//
-
-describe('t.assert(guard, [message])', function () {
-
-  var assert = t.assert;
-
-  it('should nor throw when guard is true', function () {
-    assert(true);
-  });
-
-  it('should throw a default message', function () {
-    throwsWithMessage(function () {
-      assert(1 === 2); // eslint-disable-line
-    }, '[tcomb] Assert failed');
-  });
-
-  it('should throw the specified message', function () {
-    throwsWithMessage(function () {
-      assert(1 === 2, 'my message'); // eslint-disable-line
-    }, '[tcomb] my message');
-  });
-
-  it('should handle custom fail behaviour', function () {
-    var fail = t.fail;
-    t.fail = function (message) {
-      try {
-        throw new Error(message);
-      } catch (e) {
-        eq(e.message, 'report error');
-      }
-    };
-    doesNotThrow(function () {
-      assert(1 === 2, 'report error'); // eslint-disable-line
-    });
-    t.fail = fail;
-  });
-
-});
-
-//
-// utils
-//
-
-describe('t.mixin(x, y, [overwrite])', function () {
-
-  it('should mix two objects', function () {
-    var o1 = {a: 1};
-    var o2 = {b: 2};
-    var o3 = t.mixin(o1, o2);
-    ok(o3 === o1);
-    eq(o3.a, 1);
-    eq(o3.b, 2);
-  });
-
-  it('should throw if a property already exists', function () {
-    throwsWithMessage(function () {
-      var o1 = {a: 1};
-      var o2 = {a: 2, b: 2};
-      t.mixin(o1, o2);
-    }, '[tcomb] Invalid call to mixin(): cannot overwrite property "a" of target object');
-  });
-
-  it('should not throw if a property already exists but overwrite = true', function () {
-    var o1 = {a: 1};
-    var o2 = {a: 2, b: 2};
-    var o3 = t.mixin(o1, o2, true);
-    eq(o3.a, 2);
-    eq(o3.b, 2);
-  });
-
-  it('should not mix prototype properties', function () {
-    function F() {}
-    F.prototype.method = noop;
-    var source = new F();
-    var target = {};
-    t.mixin(target, source);
-    eq(target.method, undefined);
-  });
-
-});
-
-describe('t.getTypeName(type)', function () {
-
-  var UnnamedStruct = t.struct({});
-  var NamedStruct = t.struct({}, 'NamedStruct');
-  var UnnamedUnion = t.union([t.String, t.Number]);
-  var NamedUnion = t.union([t.String, t.Number], 'NamedUnion');
-  var UnnamedMaybe = t.maybe(t.String);
-  var NamedMaybe = t.maybe(t.String, 'NamedMaybe');
-  var UnnamedEnums = t.enums({a: 'A', b: 'B'});
-  var NamedEnums = t.enums({}, 'NamedEnums');
-  var UnnamedTuple = t.tuple([t.String, t.Number]);
-  var NamedTuple = t.tuple([t.String, t.Number], 'NamedTuple');
-  var UnnamedSubtype = t.subtype(t.String, function notEmpty(x) { return x !== ''; });
-  var NamedSubtype = t.subtype(t.String, function (x) { return x !== ''; }, 'NamedSubtype');
-  var UnnamedList = t.list(t.String);
-  var NamedList = t.list(t.String, 'NamedList');
-  var UnnamedDict = t.dict(t.String, t.String);
-  var NamedDict = t.dict(t.String, t.String, 'NamedDict');
-  var UnnamedFunc = t.func(t.String, t.String);
-  var NamedFunc = t.func(t.String, t.String, 'NamedFunc');
-  var UnnamedIntersection = t.intersection([t.String, t.Number]);
-  var NamedIntersection = t.intersection([t.String, t.Number], 'NamedIntersection');
-
-  it('should return the name of a function', function () {
-    eq(t.getTypeName(function myname(){}), 'myname');
-  });
-
-  it('should return the name of a named type', function () {
-    eq(t.getTypeName(NamedStruct), 'NamedStruct');
-    eq(t.getTypeName(NamedUnion), 'NamedUnion');
-    eq(t.getTypeName(NamedMaybe), 'NamedMaybe');
-    eq(t.getTypeName(NamedEnums), 'NamedEnums');
-    eq(t.getTypeName(NamedTuple), 'NamedTuple');
-    eq(t.getTypeName(NamedSubtype), 'NamedSubtype');
-    eq(t.getTypeName(NamedList), 'NamedList');
-    eq(t.getTypeName(NamedDict), 'NamedDict');
-    eq(t.getTypeName(NamedFunc), 'NamedFunc');
-    eq(t.getTypeName(NamedIntersection), 'NamedIntersection');
-  });
-
-  it('should return a meaningful name of a unnamed type', function () {
-    eq(t.getTypeName(UnnamedStruct), '{}');
-    eq(t.getTypeName(UnnamedUnion), 'String | Number');
-    eq(t.getTypeName(UnnamedMaybe), '?String');
-    eq(t.getTypeName(UnnamedEnums), '"a" | "b"');
-    eq(t.getTypeName(UnnamedTuple), '[String, Number]');
-    eq(t.getTypeName(UnnamedSubtype), '{String | notEmpty}');
-    eq(t.getTypeName(UnnamedList), 'Array<String>');
-    eq(t.getTypeName(UnnamedDict), '{[key: String]: String}');
-    eq(t.getTypeName(UnnamedFunc), '(String) => String');
-    eq(t.getTypeName(UnnamedIntersection), 'String & Number');
-  });
-
-});
-
-//
-// Any
-//
-
-describe('t.Any', function () {
-
-  var T = t.Any;
-
-  describe('constructor', function () {
-
-    it('should behave like identity', function () {
-      eq(t.Any('a'), 'a');
-    });
-
-    it('should throw if used with new', function () {
-      throwsWithMessage(function () {
-        var x = new T(); // eslint-disable-line
-      }, '[tcomb] Cannot use the new operator to instantiate a type Any');
-    });
-
-  });
-
-  describe('#is(x)', function () {
-
-    it('should always return true', function () {
-      ok(T.is(null));
-      ok(T.is(undefined));
-      ok(T.is(0));
-      ok(T.is(true));
-      ok(T.is(''));
-      ok(T.is([]));
-      ok(T.is({}));
-      ok(T.is(noop));
-      ok(T.is(/a/));
-      ok(T.is(new RegExp('a')));
-      ok(T.is(new Error()));
-    });
-
-  });
-
-});
-
-//
-// irreducible types
-//
-
-describe('irreducibles types', function () {
-
-  it('should throw if used with an invalid name argument', function () {
-    throwsWithMessage(function () {
-      t.irreducible(null, function () { return true; });
-    }, '[tcomb] Invalid argument name = null supplied to irreducible(name, predicate)');
-  });
-
-  [
-    {T: t.Nil, x: null},
-    {T: t.String, x: 'a'},
-    {T: t.Number, x: 1},
-    {T: t.Boolean, x: true},
-    {T: t.Array, x: []},
-    {T: t.Object, x: {}},
-    {T: t.Function, x: noop},
-    {T: t.Error, x: new Error()},
-    {T: t.RegExp, x: /a/},
-    {T: t.Date, x: new Date()}
-  ].forEach(function (o) {
-
-    var T = o.T;
-    var x = o.x;
-
-    it('should accept only valid values', function () {
-      eq(T(x), x);
-    });
-
-    it('should throw if used with new', function () {
-      throwsWithMessage(function () {
-        var x = new T(); // eslint-disable-line
-      }, '[tcomb] Cannot use the new operator to instantiate a type ' + t.getTypeName(T));
-    });
-
-  });
-
-});
-
-describe('t.Nil', function () {
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is null or undefined', function () {
-      ok(t.Nil.is(null));
-      ok(t.Nil.is(undefined));
-    });
-
-    it('should return false when x is neither null nor undefined', function () {
-      ko(t.Nil.is(0));
-      ko(t.Nil.is(true));
-      ko(t.Nil.is(''));
-      ko(t.Nil.is([]));
-      ko(t.Nil.is({}));
-      ko(t.Nil.is(noop));
-      ko(t.Nil.is(new Error()));
-      ko(t.Nil.is(new Date()));
-      ko(t.Nil.is(/a/));
-      ko(t.Nil.is(new RegExp('a')));
-    });
-
-  });
-
-});
-
-describe('t.Boolean', function () {
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is true or false', function () {
-      ok(t.Boolean.is(true));
-      ok(t.Boolean.is(false));
-    });
-
-    it('should return false when x is neither true nor false', function () {
-      ko(t.Boolean.is(null));
-      ko(t.Boolean.is(undefined));
-      ko(t.Boolean.is(0));
-      ko(t.Boolean.is(''));
-      ko(t.Boolean.is([]));
-      ko(t.Boolean.is({}));
-      ko(t.Boolean.is(noop));
-      ko(t.Boolean.is(/a/));
-      ko(t.Boolean.is(new RegExp('a')));
-      ko(t.Boolean.is(new Error()));
-      ko(t.Boolean.is(new Date()));
-    });
-
-  });
-
-});
-
-describe('t.Number', function () {
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is a number', function () {
-      ok(t.Number.is(0));
-      ok(t.Number.is(1));
-      ko(t.Number.is(new Number(1))); // eslint-disable-line
-    });
-
-    it('should return false when x is not a number', function () {
-      ko(t.Number.is(NaN));
-      ko(t.Number.is(Infinity));
-      ko(t.Number.is(-Infinity));
-      ko(t.Number.is(null));
-      ko(t.Number.is(undefined));
-      ko(t.Number.is(true));
-      ko(t.Number.is(''));
-      ko(t.Number.is([]));
-      ko(t.Number.is({}));
-      ko(t.Number.is(noop));
-      ko(t.Number.is(/a/));
-      ko(t.Number.is(new RegExp('a')));
-      ko(t.Number.is(new Error()));
-      ko(t.Number.is(new Date()));
-    });
-
-  });
-
-});
-
-describe('t.String', function () {
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is a string', function () {
-      ok(t.String.is(''));
-      ok(t.String.is('a'));
-      /* jshint ignore:start */
-      ko(t.String.is(new String('a'))); // eslint-disable-line
-      /* jshint ignore:end */
-    });
-
-    it('should return false when x is not a string', function () {
-      ko(t.String.is(NaN));
-      ko(t.String.is(Infinity));
-      ko(t.String.is(-Infinity));
-      ko(t.String.is(null));
-      ko(t.String.is(undefined));
-      ko(t.String.is(true));
-      ko(t.String.is(1));
-      ko(t.String.is([]));
-      ko(t.String.is({}));
-      ko(t.String.is(noop));
-      ko(t.String.is(/a/));
-      ko(t.String.is(new RegExp('a')));
-      ko(t.String.is(new Error()));
-      ko(t.String.is(new Date()));
-    });
-
-  });
-
-});
-
-describe('t.Array', function () {
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is an array', function () {
-      ok(t.Array.is([]));
-    });
-
-    it('should return false when x is not an array', function () {
-      ko(t.Array.is(NaN));
-      ko(t.Array.is(Infinity));
-      ko(t.Array.is(-Infinity));
-      ko(t.Array.is(null));
-      ko(t.Array.is(undefined));
-      ko(t.Array.is(true));
-      ko(t.Array.is(1));
-      ko(t.Array.is('a'));
-      ko(t.Array.is({}));
-      ko(t.Array.is(noop));
-      ko(t.Array.is(/a/));
-      ko(t.Array.is(new RegExp('a')));
-      ko(t.Array.is(new Error()));
-      ko(t.Array.is(new Date()));
-    });
-
-  });
-
-});
-
-describe('t.Object', function () {
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is an object', function () {
-      function A() {}
-      ok(t.Object.is({}));
-      ok(t.Object.is(new A()));
-    });
-
-    it('should return false when x is not an object', function () {
-      ko(t.Object.is(null));
-      ko(t.Object.is(undefined));
-      ko(t.Object.is(0));
-      ko(t.Object.is(''));
-      ko(t.Object.is([]));
-      ko(t.Object.is(noop));
-    });
-
-  });
-
-});
-
-describe('t.Function', function () {
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is a function', function () {
-      ok(t.Function.is(noop));
-      ok(t.Function.is(new Function())); // eslint-disable-line
-    });
-
-    it('should return false when x is not a function', function () {
-      ko(t.Function.is(null));
-      ko(t.Function.is(undefined));
-      ko(t.Function.is(0));
-      ko(t.Function.is(''));
-      ko(t.Function.is([]));
-      ko(t.Function.is({}));
-      ko(t.Function.is(new String('1'))); // eslint-disable-line
-      ko(t.Function.is(new Number(1))); // eslint-disable-line
-      ko(t.Function.is(new Boolean())); // eslint-disable-line
-      ko(t.Function.is(/a/));
-      ko(t.Function.is(new RegExp('a')));
-      ko(t.Function.is(new Error()));
-      ko(t.Function.is(new Date()));
-    });
-
-  });
-
-});
-
-describe('t.Error', function () {
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is an error', function () {
-      ok(t.Error.is(new Error()));
-    });
-
-    it('should return false when x is not an error', function () {
-      ko(t.Error.is(null));
-      ko(t.Error.is(undefined));
-      ko(t.Error.is(0));
-      ko(t.Error.is(''));
-      ko(t.Error.is([]));
-      ko(t.Error.is(new String('1'))); // eslint-disable-line
-      ko(t.Error.is(new Number(1))); // eslint-disable-line
-      ko(t.Error.is(new Boolean())); // eslint-disable-line
-      ko(t.Error.is(/a/));
-      ko(t.Error.is(new RegExp('a')));
-      ko(t.Error.is(new Date()));
-    });
-
-  });
-
-});
-
-describe('t.RegExp', function () {
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is a regexp', function () {
-      ok(t.RegExp.is(/a/));
-      ok(t.RegExp.is(new RegExp('a')));
-    });
-
-    it('should return false when x is not a regexp', function () {
-      ko(t.RegExp.is(null));
-      ko(t.RegExp.is(undefined));
-      ko(t.RegExp.is(0));
-      ko(t.RegExp.is(''));
-      ko(t.RegExp.is([]));
-      ko(t.RegExp.is(new String('1'))); // eslint-disable-line
-      ko(t.RegExp.is(new Number(1))); // eslint-disable-line
-      ko(t.RegExp.is(new Boolean())); // eslint-disable-line
-      ko(t.RegExp.is(new Error()));
-      ko(t.RegExp.is(new Date()));
-    });
-
-  });
-
-});
-
-describe('t.Date', function () {
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is a Dat', function () {
-      ok(t.Date.is(new Date()));
-    });
-
-    it('should return false when x is not a Dat', function () {
-      ko(t.Date.is(null));
-      ko(t.Date.is(undefined));
-      ko(t.Date.is(0));
-      ko(t.Date.is(''));
-      ko(t.Date.is([]));
-      ko(t.Date.is(new String('1'))); // eslint-disable-line
-      ko(t.Date.is(new Number(1))); // eslint-disable-line
-      ko(t.Date.is(new Boolean())); // eslint-disable-line
-      ko(t.Date.is(new Error()));
-      ko(t.Date.is(/a/));
-      ko(t.Date.is(new RegExp('a')));
-    });
-
-  });
-
-});
-
-//
-// struct
-//
-
-describe('t.struct(props, [name])', function () {
-
-  describe('combinator', function () {
-
-    it('should throw if used with wrong arguments', function () {
-
-      throwsWithMessage(function () {
-        t.struct();
-      }, '[tcomb] Invalid argument props = undefined supplied to struct(props, name): expected a dictionary of tcomb types');
-
-      throwsWithMessage(function () {
-        t.struct({a: null});
-      }, '[tcomb] Invalid argument props = {\n  "a": null\n} supplied to struct(props, name): expected a dictionary of tcomb types');
-
-      throwsWithMessage(function () {
-        t.struct({}, 1);
-      }, '[tcomb] Invalid argument name = 1 supplied to struct(props, name): expected a string');
-
-    });
-
-  });
-
-  describe('constructor', function () {
-
-    it('should be idempotent', function () {
-      var T = Point;
-      var p1 = T({x: 0, y: 0});
-      var p2 = T(p1);
-      eq(Object.isFrozen(p1), true);
-      eq(Object.isFrozen(p2), true);
-      eq(p2 === p1, true);
-    });
-
-    it('should accept only valid values', function () {
-      throwsWithMessage(function () {
-        Point(1);
-      }, '[tcomb] Invalid argument value = 1 supplied to struct {x: Number, y: Number}: expected an object');
-    });
-
-  });
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is an instance of the struct', function () {
-      var p = new Point({ x: 1, y: 2 });
-      ok(Point.is(p));
-    });
-
-  });
-
-  describe('#update()', function () {
-
-    var Type = t.struct({name: t.String});
-    var instance = new Type({name: 'Giulio'});
-
-    it('should return a new instance', function () {
-      var newInstance = Type.update(instance, {name: {$set: 'Canti'}});
-      ok(Type.is(newInstance));
-      eq(instance.name, 'Giulio');
-      eq(newInstance.name, 'Canti');
-    });
-
-  });
-
-  describe('#extend(props, [name])', function () {
-
-    it('should extend an existing struct', function () {
-      var Point = t.struct({
-        x: t.Number,
-        y: t.Number
-      }, 'Point');
-      var Point3D = Point.extend({z: t.Number}, 'Point3D');
-      eq(t.getTypeName(Point3D), 'Point3D');
-      eq(Point3D.meta.props.x, t.Number);
-      eq(Point3D.meta.props.y, t.Number);
-      eq(Point3D.meta.props.z, t.Number);
-    });
-
-    it('should handle an array as argument', function () {
-      var Type = t.struct({a: t.String}, 'Type');
-      var Mixin = [{b: t.Number, c: t.Boolean}];
-      var NewType = Type.extend(Mixin, 'NewType');
-      eq(t.getTypeName(NewType), 'NewType');
-      eq(NewType.meta.props.a, t.String);
-      eq(NewType.meta.props.b, t.Number);
-      eq(NewType.meta.props.c, t.Boolean);
-    });
-
-    it('should handle a struct (or list of structs) as argument', function () {
-      var A = t.struct({a: t.String}, 'A');
-      var B = t.struct({b: t.String}, 'B');
-      var C = t.struct({c: t.String}, 'C');
-      var MixinD = {d: t.String};
-      var E = A.extend([B, C, MixinD]);
-      eq(E.meta.props, {
-        a: t.String,
-        b: t.String,
-        c: t.String,
-        d: t.String
-      });
-    });
-
-    it('should support prototypal inheritance', function () {
-      var Rectangle = t.struct({
-        w: t.Number,
-        h: t.Number
-      }, 'Rectangle');
-      Rectangle.prototype.area = function () {
-        return this.w * this.h;
-      };
-      var Cube = Rectangle.extend({
-        l: t.Number
-      });
-      Cube.prototype.volume = function () {
-        return this.area() * this.l;
-      };
-
-      assert(typeof Rectangle.prototype.area === 'function');
-      assert(typeof Cube.prototype.area === 'function');
-      assert(undefined === Rectangle.prototype.volume);
-      assert(typeof Cube.prototype.volume === 'function');
-      assert(Cube.prototype.constructor === Cube);
-
-      var c = new Cube({w: 2, h: 2, l: 2});
-      eq(c.volume(), 8);
-    });
-
-  });
-
-});
-
-//
-// enums
-//
-
-describe('t.enums(map, [name])', function () {
-
-  describe('combinator', function () {
-
-    it('should throw if used with wrong arguments', function () {
-
-      throwsWithMessage(function () {
-        t.enums();
-      }, '[tcomb] Invalid argument map = undefined supplied to enums(map, name): expected a hash of strings / numbers');
-
-      throwsWithMessage(function () {
-        t.enums({}, 1);
-      }, '[tcomb] Invalid argument name = 1 supplied to enums(map, name): expected a string');
-
-    });
-
-  });
-
-  describe('constructor', function () {
-
-    var T = t.enums({a: 0}, 'T');
-
-    it('should throw if used with new', function () {
-      throwsWithMessage(function () {
-        var x = new T('a'); // eslint-disable-line
-      }, '[tcomb] Cannot use the new operator to instantiate a type T');
-    });
-
-    it('should accept only valid values', function () {
-      eq(T('a'), 'a');
-      throwsWithMessage(function () {
-        T('b');
-      }, '[tcomb] Invalid argument value = "b" supplied to enums T: expected one of [\n  "a"\n]');
-    });
-
-  });
-
-  describe('#is(x)', function () {
-
-    var Direction = t.enums({
-      North: 0,
-      East: 1,
-      South: 2,
-      West: 3,
-      1: 'North-East',
-      2.5: 'South-East'
-    });
-
-    it('should return true when x is an instance of the enum', function () {
-      ok(Direction.is('North'));
-      ok(Direction.is(1));
-      ok(Direction.is('1'));
-      ok(Direction.is(2.5));
-    });
-
-    it('should return false when x is not an instance of the enum', function () {
-      ko(Direction.is('North-East'));
-      ko(Direction.is(2));
-    });
-
-  });
-
-  describe('#of(keys)', function () {
-
-    it('should return an enum', function () {
-      var Size = t.enums.of(['large', 'small', 1, 10.9]);
-      ok(Size.meta.map.large === 'large');
-      ok(Size.meta.map.small === 'small');
-      ok(Size.meta.map['1'] === 1);
-      ok(Size.meta.map[10.9] === 10.9);
-    });
-
-    it('should handle a string', function () {
-      var Size = t.enums.of('large small 10');
-      ok(Size.meta.map.large === 'large');
-      ok(Size.meta.map.small === 'small');
-      ok(Size.meta.map['10'] === '10');
-      ok(Size.meta.map[10] === '10');
-    });
-
-  });
-
-});
-
-//
-// union
-//
-
-describe('t.union(types, [name])', function () {
-
-  var Circle = t.struct({
-    center: Point,
-    radius: t.Number
-  }, 'Circle');
-
-  var Rectangle = t.struct({
-    a: Point,
-    b: Point
-  });
-
-  var Shape = t.union([Circle, Rectangle], 'Shape');
-
-  Shape.dispatch = function (values) {
-    assert(t.Object.is(values));
-    return values.hasOwnProperty('center') ?
-      Circle :
-      Rectangle;
-  };
-
-  describe('combinator', function () {
-
-    it('should throw if used with wrong arguments', function () {
-
-      throwsWithMessage(function () {
-        t.union();
-      }, '[tcomb] Invalid argument types = undefined supplied to union(types, name): expected an array of at least 2 types');
-
-      throwsWithMessage(function () {
-        t.union([]);
-      }, '[tcomb] Invalid argument types = [] supplied to union(types, name): expected an array of at least 2 types');
-
-      throwsWithMessage(function () {
-        t.union([1]);
-      }, '[tcomb] Invalid argument types = [\n  1\n] supplied to union(types, name): expected an array of at least 2 types');
-
-      throwsWithMessage(function () {
-        t.union([Circle, Point], 1);
-      }, '[tcomb] Invalid argument name = 1 supplied to union(types, name): expected a string');
-
-    });
-
-  });
-
-  describe('constructor', function () {
-
-    it('should throw when dispatch() is not implemented', function () {
-      throwsWithMessage(function () {
-        var T = t.union([t.String, t.Number], 'T');
-        T.dispatch = null;
-        T(1);
-      }, '[tcomb] Unimplemented dispatch() function for union T');
-    });
-
-    it('should have a default dispatch() implementation', function () {
-      var T = t.union([t.String, t.Number], 'T');
-      eq(T(1), 1);
-    });
-
-    it('should throw when dispatch() does not return a type', function () {
-      throwsWithMessage(function () {
-        var T = t.union([t.String, t.Number], 'T');
-        T(true);
-      }, '[tcomb] The dispatch() function of union T returns no type');
-    });
-
-    it('should build instances when dispatch() is implemented', function () {
-      var circle = Shape({center: {x: 0, y: 0}, radius: 10});
-      ok(Circle.is(circle));
-    });
-
-    it('should throw if used with new and union types are not instantiables with new', function () {
-      throwsWithMessage(function () {
-        var T = t.union([t.String, t.Number], 'T');
-        T.dispatch = function () { return t.String; };
-        var x = new T('a'); // eslint-disable-line
-      }, '[tcomb] Cannot use the new operator to instantiate a type T');
-    });
-
-    it('should not throw if used with new and union types are instantiables with new', function () {
-      doesNotThrow(function () {
-        Shape({center: {x: 0, y: 0}, radius: 10});
-      });
-    });
-
-    it('should be idempotent', function () {
-      var p1 = Shape({center: {x: 0, y: 0}, radius: 10});
-      var p2 = Shape(p1);
-      eq(Object.isFrozen(p1), true);
-      eq(Object.isFrozen(p2), true);
-      eq(p2 === p1, true);
-    });
-
-  });
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is an instance of the union', function () {
-      var p = new Circle({center: { x: 0, y: 0 }, radius: 10});
-      ok(Shape.is(p));
-    });
-
-  });
-
-});
-
-//
-// intersection
-//
-
-describe('t.intersection(types, [name])', function () {
-
-  var Min = t.subtype(t.String, function (s) { return s.length > 2; }, 'Min');
-  var Max = t.subtype(t.String, function (s) { return s.length < 5; }, 'Max');
-  var MinMax = t.intersection([Min, Max], 'MinMax');
-
-  describe('combinator', function () {
-
-    it('should throw if used with wrong arguments', function () {
-
-      throwsWithMessage(function () {
-        t.intersection();
-      }, '[tcomb] Invalid argument types = undefined supplied to intersection(types, name): expected an array of at least 2 types');
-
-      throwsWithMessage(function () {
-        t.intersection([]);
-      }, '[tcomb] Invalid argument types = [] supplied to intersection(types, name): expected an array of at least 2 types');
-
-      throwsWithMessage(function () {
-        t.intersection([1]);
-      }, '[tcomb] Invalid argument types = [\n  1\n] supplied to intersection(types, name): expected an array of at least 2 types');
-
-      throwsWithMessage(function () {
-        t.intersection([Min, Max], 1);
-      }, '[tcomb] Invalid argument name = 1 supplied to intersection(types, name): expected a string');
-
-    });
-
-  });
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is an instance of the intersection', function () {
-      ok(MinMax.is('123'), '3 chars');
-      ko(MinMax.is('12'), '2 chars');
-      ko(MinMax.is('12345'), '5 chars');
-    });
-
-  });
-
-});
-
-//
-// maybe
-//
-
-describe('t.maybe(type, [name])', function () {
-
-  describe('combinator', function () {
-
-    it('should throw if used with wrong arguments', function () {
-
-      throwsWithMessage(function () {
-        t.maybe();
-      }, '[tcomb] Invalid argument type = undefined supplied to maybe(type, name): expected a type');
-
-      throwsWithMessage(function () {
-        t.maybe(Point, 1);
-      }, '[tcomb] Invalid argument name = 1 supplied to maybe(type, name): expected a string');
-
-    });
-
-    it('should be idempotent', function () {
-      var MaybeStr = t.maybe(t.String);
-      ok(t.maybe(MaybeStr) === MaybeStr);
-    });
-
-    it('should be noop with Any', function () {
-      ok(t.maybe(t.Any) === t.Any);
-    });
-
-    it('should be noop with Nil', function () {
-      ok(t.maybe(t.Nil) === t.Nil);
-    });
-
-  });
-
-  describe('constructor', function () {
-
-    it('should throw if used with new', function () {
-      throwsWithMessage(function () {
-        var T = t.maybe(t.String, 'T');
-        var x = new T(); // eslint-disable-line
-      }, '[tcomb] Cannot use the new operator to instantiate a type T');
-    });
-
-    it('should coerce values', function () {
-      var T = t.maybe(Point);
-      eq(T(null), null);
-      eq(T(undefined), null);
-      ok(Point.is(T({x: 0, y: 0})));
-    });
-
-    it('should be idempotent', function () {
-      var T = t.maybe(Point);
-      var p1 = T({x: 0, y: 0});
-      var p2 = T(p1);
-      eq(Object.isFrozen(p1), true);
-      eq(Object.isFrozen(p2), true);
-      eq(p2 === p1, true);
-    });
-
-  });
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is an instance of the maybe', function () {
-      var Radio = t.maybe(t.String);
-      ok(Radio.is('a'));
-      ok(Radio.is(null));
-      ok(Radio.is(undefined));
-    });
-
-  });
-
-});
-
-//
-// tuple
-//
-
-describe('t.tuple(types, [name])', function () {
-
-  var Area = t.tuple([t.Number, t.Number], 'Area');
-
-  describe('combinator', function () {
-
-    it('should throw if used with wrong arguments', function () {
-
-      throwsWithMessage(function () {
-        t.tuple();
-      }, '[tcomb] Invalid argument types = undefined supplied to tuple(types, name): expected an array of types');
-
-      throwsWithMessage(function () {
-        t.tuple([1]);
-      }, '[tcomb] Invalid argument types = [\n  1\n] supplied to tuple(types, name): expected an array of types');
-
-      throwsWithMessage(function () {
-        t.tuple([Point, Point], 1);
-      }, '[tcomb] Invalid argument name = 1 supplied to tuple(types, name): expected a string');
-
-    });
-
-  });
-
-  describe('constructor', function () {
-
-    var S = t.struct({}, 'S');
-    var T = t.tuple([S, S], 'T');
-
-    it('should coerce values', function () {
-      var t = T([{}, {}]);
-      ok(S.is(t[0]));
-      ok(S.is(t[1]));
-    });
-
-    it('should accept only valid values', function () {
-
-      throwsWithMessage(function () {
-        T(1);
-      }, '[tcomb] Invalid argument value = 1 supplied to tuple T: expected an array of length 2');
-
-      throwsWithMessage(function () {
-        T([1, 1]);
-      }, '[tcomb] Invalid argument value = 1 supplied to struct S: expected an object');
-
-    });
-
-    it('should be idempotent', function () {
-      var T = t.tuple([t.String, t.Number]);
-      var p1 = T(['a', 1]);
-      var p2 = T(p1);
-      eq(Object.isFrozen(p1), true);
-      eq(Object.isFrozen(p2), true);
-      eq(p2 === p1, true);
-    });
-
-  });
-
-  describe('#is(x)', function () {
-
-    it('should return true when x is an instance of the tuple', function () {
-      ok(Area.is([1, 2]));
-    });
-
-    it('should return false when x is not an instance of the tuple', function () {
-      ko(Area.is([1]));
-      ko(Area.is([1, 2, 3]));
-      ko(Area.is([1, 'a']));
-    });
-
-    it('should not depend on `this`', function () {
-      ok([[1, 2]].every(Area.is));
-    });
-
-  });
-
-  describe('#update()', function () {
-
-    var Type = t.tuple([t.String, t.Number]);
-    var instance = Type(['a', 1]);
-
-    it('should return a new instance', function () {
-      var newInstance = Type.update(instance, {0: {$set: 'b'}});
-      assert(Type.is(newInstance));
-      assert(instance[0] === 'a');
-      assert(newInstance[0] === 'b');
-    });
-
-  });
-
-});
-
-//
-// list
-//
-
-describe('t.list(type, [name])', function () {
-
-  describe('combinator', function () {
-
-    it('should throw if used with wrong arguments', function () {
-
-      throwsWithMessage(function () {
-        t.list();
-      }, '[tcomb] Invalid argument type = undefined supplied to list(type, name): expected a type');
-
-      throwsWithMessage(function () {
-        t.list(Point, 1);
-      }, '[tcomb] Invalid argument name = 1 supplied to list(type, name): expected a string');
-
-    });
-
-  });
-
-  describe('constructor', function () {
-
-    var S = t.struct({}, 'S');
-    var T = t.list(S, 'T');
-
-    it('should coerce values', function () {
-      var t = T([{}]);
-      ok(S.is(t[0]));
-    });
-
-    it('should accept only valid values', function () {
-
-      throwsWithMessage(function () {
-        T(1);
-      }, '[tcomb] Invalid argument value = 1 supplied to list T');
-
-      throwsWithMessage(function () {
-        T([1]);
-      }, '[tcomb] Invalid argument value = 1 supplied to struct S: expected an object');
-
-    });
-
-    it('should be idempotent', function () {
-      var T = t.list(t.Number);
-      var p1 = T([1, 2]);
-      var p2 = T(p1);
-      eq(Object.isFrozen(p1), true);
-      eq(Object.isFrozen(p2), true);
-      eq(p2 === p1, true);
-    });
-
-  });
-
-  describe('#is(x)', function () {
-
-    var Path = t.list(Point);
-    var p1 = new Point({x: 0, y: 0});
-    var p2 = new Point({x: 1, y: 1});
-
-    it('should return true when x is a list', function () {
-      ok(Path.is([p1, p2]));
-    });
-
-    it('should not depend on `this`', function () {
-      ok([[p1, p2]].every(Path.is));
-    });
-
-  });
-
-  describe('#update()', function () {
-
-    var Type = t.list(t.String);
-    var instance = Type(['a', 'b']);
-
-    it('should return a new instance', function () {
-      var newInstance = Type.update(instance, {'$push': ['c']});
-      assert(Type.is(newInstance));
-      assert(instance.length === 2);
-      assert(newInstance.length === 3);
-    });
-
-  });
-
-});
-
-//
-// subtype
-//
-
-describe('t.subtype(type, predicate, [name])', function () {
-
-  var True = function () { return true; };
-
-  describe('combinator', function () {
-
-    it('should throw if used with wrong arguments', function () {
-
-      throwsWithMessage(function () {
-        t.subtype();
-      }, '[tcomb] Invalid argument type = undefined supplied to subtype(type, predicate, name): expected a type');
-
-      throwsWithMessage(function () {
-        t.subtype(Point, null);
-      }, '[tcomb] Invalid argument predicate supplied to subtype(type, predicate, name): expected a function');
-
-      throwsWithMessage(function () {
-        t.subtype(Point, True, 1);
-      }, '[tcomb] Invalid argument name = 1 supplied to subtype(type, predicate, name): expected a string');
-
-    });
-
-  });
-
-  describe('constructor', function () {
-
-    it('should throw if used with new and a type that is not instantiable with new', function () {
-      throwsWithMessage(function () {
-        var T = t.subtype(t.String, function () { return true; }, 'T');
-        var x = new T(); // eslint-disable-line
-      }, '[tcomb] Cannot use the new operator to instantiate a type T');
-    });
-
-    it('should coerce values', function () {
-      var T = t.subtype(Point, function () { return true; });
-      var p = T({x: 0, y: 0});
-      ok(Point.is(p));
-    });
-
-    it('should accept only valid values', function () {
-      var predicate = function (p) { return p.x > 0; };
-      var T = t.subtype(Point, predicate, 'T');
-      throwsWithMessage(function () {
-        T({x: 0, y: 0});
-      }, '[tcomb] Invalid argument value = {\n  "x": 0,\n  "y": 0\n} supplied to subtype T');
-    });
-
-  });
-
-  describe('#is(x)', function () {
-
-    var Positive = t.subtype(t.Number, function (n) {
-      return n >= 0;
-    });
-
-    it('should return true when x is a subtype', function () {
-      ok(Positive.is(1));
-    });
-
-    it('should return false when x is not a subtype', function () {
-      ko(Positive.is(-1));
-    });
-
-  });
-
-  describe('#update()', function () {
-
-    var Type = t.subtype(t.String, function (s) { return s.length > 2; });
-    var instance = Type('abc');
-
-    it('should return a new instance', function () {
-      var newInstance = Type.update(instance, {'$set': 'bca'});
-      assert(Type.is(newInstance));
-      eq(newInstance, 'bca');
-    });
-
-  });
-
-});
-
-//
-// dict
-//
-
-describe('t.dict(domain, codomain, [name])', function () {
-
-  describe('combinator', function () {
-
-    it('should throw if used with wrong arguments', function () {
-
-      throwsWithMessage(function () {
-        t.dict();
-      }, '[tcomb] Invalid argument domain = undefined supplied to dict(domain, codomain, name): expected a type');
-
-      throwsWithMessage(function () {
-        t.dict(t.String);
-      }, '[tcomb] Invalid argument codomain = undefined supplied to dict(domain, codomain, name): expected a type');
-
-      throwsWithMessage(function () {
-        t.dict(t.String, Point, 1);
-      }, '[tcomb] Invalid argument name = 1 supplied to dict(domain, codomain, name): expected a string');
-
-    });
-
-  });
-
-  describe('constructor', function () {
-
-    var S = t.struct({}, 'S');
-    var Domain = t.subtype(t.String, function (x) {
-      return x !== 'forbidden';
-    }, 'Domain');
-    var T = t.dict(Domain, S, 'T');
-
-    it('should coerce values', function () {
-      var t = T({a: {}});
-      ok(S.is(t.a));
-    });
-
-    it('should accept only valid values', function () {
-
-      throwsWithMessage(function () {
-        T(1);
-      }, '[tcomb] Invalid argument value = 1 supplied to dict T');
-
-      throwsWithMessage(function () {
-        T({a: 1});
-      }, '[tcomb] Invalid argument value = 1 supplied to struct S: expected an object');
-
-      throwsWithMessage(function () {
-        T({forbidden: {}});
-      }, '[tcomb] Invalid argument value = "forbidden" supplied to subtype Domain');
-
-    });
-
-    it('should be idempotent', function () {
-      var T = t.dict(t.String, t.String);
-      var p1 = T({a: 'a', b: 'b'});
-      var p2 = T(p1);
-      eq(Object.isFrozen(p1), true);
-      eq(Object.isFrozen(p2), true);
-      eq(p2 === p1, true);
-    });
-
-  });
-
-  describe('#is(x)', function () {
-
-    var T = t.dict(t.String, Point);
-    var p1 = new Point({x: 0, y: 0});
-    var p2 = new Point({x: 1, y: 1});
-
-    it('should return true when x is a list', function () {
-      ok(T.is({a: p1, b: p2}));
-    });
-
-    it('should not depend on `this`', function () {
-      ok([{a: p1, b: p2}].every(T.is));
-    });
-
-  });
-
-  describe('#update()', function () {
-
-    var Type = t.dict(t.String, t.String);
-    var instance = Type({p1: 'a', p2: 'b'});
-
-    it('should return a new instance', function () {
-      var newInstance = Type.update(instance, {p2: {$set: 'c'}});
-      ok(Type.is(newInstance));
-      eq(instance.p2, 'b');
-      eq(newInstance.p2, 'c');
-    });
-
-  });
-
-});
-
-//
-// func
-//
-
-describe('t.func(domain, codomain, [name])', function () {
-
-  it('should handle a no types', function () {
-    var T = t.func([], t.String);
-    eq(T.meta.domain.length, 0);
-    var getGreeting = T.of(function () { return 'Hi'; });
-    eq(getGreeting(), 'Hi');
-  });
-
-  it('should handle a single type', function () {
-    var T = t.func(t.Number, t.Number);
-    eq(T.meta.domain.length, 1);
-    ok(T.meta.domain[0] === t.Number);
-  });
-
-  it('should automatically instrument a function', function () {
-    var T = t.func(t.Number, t.Number);
-    var f = function () { return 'hi'; };
-    ok(T.is(T(f)));
-  });
-
-  describe('of', function () {
-
-    it('should check the arguments', function () {
-
-      var T = t.func([t.Number, t.Number], t.Number);
-      var sum = T.of(function (a, b) {
-        return a + b;
-      });
-      eq(sum(1, 2), 3);
-
-      throwsWithMessage(function () {
-        sum(1, 2, 3);
-      }, '[tcomb] Invalid argument value = [\n  1,\n  2,\n  3\n] supplied to tuple [Number, Number]: expected an array of length 2');
-
-      throwsWithMessage(function () {
-        sum('a', 2);
-      }, '[tcomb] Invalid argument value = "a" supplied to irreducible type Number');
-
-    });
-
-    it('should check the return value', function () {
-
-      var T = t.func([t.Number, t.Number], t.Number);
-      var sum = T.of(function () {
-        return 'a';
-      });
-
-      throwsWithMessage(function () {
-        sum(1, 2);
-      }, '[tcomb] Invalid argument value = "a" supplied to irreducible type Number');
-
-    });
-
-    it('should preserve `this`', function () {
-      var o = {name: 'giulio'};
-      o.getTypeName = t.func([], t.String).of(function () {
-        return this.name;
-      });
-      eq(o.getTypeName(), 'giulio');
-    });
-
-    it('should handle function types', function () {
-      var A = t.func([t.String], t.String);
-      var B = t.func([t.String, A], t.String);
-
-      var f = A.of(function (s) {
-        return s + '!';
-      });
-      var g = B.of(function (str, strAction) {
-        return strAction(str);
-      });
-
-      eq(g('hello', f), 'hello!');
-    });
-
-    it('should be idempotent', function () {
-      var f = function (s) { return s; };
-      var g = t.func([t.String], t.String).of(f);
-      var h = t.func([t.String], t.String).of(g);
-      ok(h === g);
-    });
-
-  });
-
-  describe('currying', function () {
-
-    it('should curry functions', function () {
-      var Type = t.func([t.Number, t.Number, t.Number], t.Number);
-      var sum = Type.of(function (a, b, c) {
-        return a + b + c;
-      }, true);
-      eq(sum(1, 2, 3), 6);
-      eq(sum(1, 2)(3), 6);
-      eq(sum(1)(2, 3), 6);
-      eq(sum(1)(2)(3), 6);
-
-      // important: the curried function must be of the correct type
-      var CurriedType = t.func([t.Number, t.Number], t.Number);
-      var sum1 = sum(1);
-      eq(sum1(2, 3), 6);
-      eq(sum1(2)(3), 6);
-      ok(CurriedType.is(sum1));
-    });
-
-    it('should throw if partial arguments are wrong', function () {
-
-      var T = t.func([t.Number, t.Number], t.Number);
-      var sum = T.of(function (a, b) {
-        return a + b;
-      }, true);
-
-      throwsWithMessage(function () {
-        sum('a');
-      }, '[tcomb] Invalid argument value = "a" supplied to irreducible type Number');
-
-      throwsWithMessage(function () {
-        var sum1 = sum(1);
-        sum1('a');
-      }, '[tcomb] Invalid argument value = "a" supplied to irreducible type Number');
-
-    });
-
-  });
-
-  describe('uncurried', function () {
-
-    it('should not curry functions', function () {
-      var Type = t.func([t.Number, t.Number, t.Number], t.Number);
-      var sum = Type.of(function (a, b, c) {
-        return a + b + c;
-      });
-      eq(sum(1, 2, 3), 6);
-      throwsWithMessage(function () {
-        sum(1, 2);
-      }, '[tcomb] Invalid argument value = [\n  1,\n  2\n] supplied to tuple [Number, Number, Number]: expected an array of length 3');
-    });
-
-  });
-
-});
-
-describe('ES6 classes', function () {
-
-  function Class(a) {
-    this.a = a;
-  }
-
-  var c = new Class('a');
-
-  it('should be handled by subtype', function () {
-    var T = t.subtype(Class, function isA(x) {
-      return x.a === 'a';
-    });
-    eq(T.is(c), true);
-    throwsWithMessage(function () {
-      T(new Class('b'));
-    }, '[tcomb] Invalid argument value = {\n  "a": "b"\n} supplied to subtype {Class | isA}');
-  });
-
-  it('should be handled by struct', function () {
-    var T = t.struct({
-      c: Class
-    });
-    eq(T.is(new T({c: c})), true);
-    throwsWithMessage(function () {
-      T({c: 1});
-    }, '[tcomb] The value 1 is not an instance of Class');
-  });
-
-  it('should be handled by maybe', function () {
-    var T = t.maybe(Class);
-    eq(T.is(null), true);
-    eq(T.is(c), true);
-    throwsWithMessage(function () {
-      T(1);
-    }, '[tcomb] The value 1 is not an instance of Class');
-  });
-
-  it('should be handled by tuple', function () {
-    var T = t.tuple([Class]);
-    eq(T.is([c]), true);
-    throwsWithMessage(function () {
-      T([1]);
-    }, '[tcomb] The value 1 is not an instance of Class');
-  });
-
-  it('should be handled by list', function () {
-    var T = t.list(Class);
-    eq(T.is([c]), true);
-    throwsWithMessage(function () {
-      T([1]);
-    }, '[tcomb] The value 1 is not an instance of Class');
-  });
-
-  it('should be handled by dict', function () {
-    var T = t.dict(t.String, Class);
-    eq(T.is({a: c}), true);
-    throwsWithMessage(function () {
-      T({a: 1});
-    }, '[tcomb] The value 1 is not an instance of Class');
-  });
-
-  it('should be handled by union', function () {
-    var T = t.union([t.String, Class]);
-    eq(T.is(c), true);
-    throwsWithMessage(function () {
-      T(1);
-    }, '[tcomb] The dispatch() function of union String | Class returns no type');
-  });
-
-  it('should be handled by func', function () {
-    var T = t.func(Class, t.String);
-    var f = T.of(function (c) {
-      return c.constructor.name;
-    });
-    eq(f(c), 'Class');
-    throwsWithMessage(function () {
-      f(1);
-    }, '[tcomb] The value 1 is not an instance of Class');
-  });
-
-});
-
-describe('match', function () {
-
-  it('should match on type constructors', function () {
-    eq(t.match(1,
-      t.String, function () { return 'a string'; },
-      t.Number, function (n) { return 2 * n; }
-    ), 2);
-  });
-
-  it('should handle an optional guard', function () {
-    eq(t.match(1,
-      t.String, function () { return 'a string'; },
-      t.Number, function () { return false; }, function (n) { return 2 * n; },
-      t.Number, function (n) { return 3 * n; }
-    ), 3);
-  });
-
-  it('should throw if no match is found', function () {
-    throwsWithMessage(function () {
-      t.match(true,
-        t.String, function () { return 'a string'; },
-        t.Number, function (n) { return 2 * n; }
-      );
-    }, '[tcomb] Match error');
-  });
-
-  it('should throw if cases are misplaced', function () {
-    throwsWithMessage(function () {
-      t.match(true,
-        t.String, function () { return 'a string'; },
-        t.Number
-      );
-    }, '[tcomb] Invalid block in clause #2');
-  });
-
-  it('should handle unions of unions', function () {
-    var A = t.subtype(t.String, function (s) { return s === 'A'; });
-    var B = t.subtype(t.String, function (s) { return s === 'B'; });
-    var C = t.subtype(t.String, function (s) { return s === 'C'; });
-    var D = t.subtype(t.String, function (s) { return s === 'D'; });
-    var E = t.subtype(t.String, function (s) { return s === 'E'; });
-    var F = t.subtype(t.String, function (s) { return s === 'F'; });
-    var G = t.subtype(t.String, function (s) { return s === 'G'; });
-    var H = t.subtype(t.String, function (s) { return s === 'H'; });
-    var U1 = t.union([A, B]);
-    var U2 = t.union([C, D]);
-    var U3 = t.union([E, F]);
-    var U4 = t.union([G, H]);
-    var UU1 = t.union([U1, U2]);
-    var UU2 = t.union([U3, U4]);
-    eq(t.match('F',
-      UU1, function () { return '1'; },
-      UU2, function () { return '2'; }
-    ), '2');
-  });
-
-});
diff --git a/test/tuple.js b/test/tuple.js
new file mode 100644
index 0000000..6d775f8
--- /dev/null
+++ b/test/tuple.js
@@ -0,0 +1,101 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+var Point = t.struct({
+  x: t.Number,
+  y: t.Number
+});
+
+describe('t.tuple(types, [name])', function () {
+
+  var Area = t.tuple([t.Number, t.Number], 'Area');
+
+  describe('combinator', function () {
+
+    it('should throw if used with wrong arguments', function () {
+
+      throwsWithMessage(function () {
+        t.tuple();
+      }, '[tcomb] Invalid argument types undefined supplied to tuple(types, [name]) combinator (expected an array of types)');
+
+      throwsWithMessage(function () {
+        t.tuple([1]);
+      }, '[tcomb] Invalid argument types [\n  1\n] supplied to tuple(types, [name]) combinator (expected an array of types)');
+
+      throwsWithMessage(function () {
+        t.tuple([Point, Point], 1);
+      }, '[tcomb] Invalid argument name 1 supplied to tuple(types, [name]) combinator (expected a string)');
+
+    });
+
+  });
+
+  describe('constructor', function () {
+
+    var S = t.struct({}, 'S');
+    var T = t.tuple([S, S], 'T');
+
+    it('should coerce values', function () {
+      var t = T([{}, {}]);
+      assert.ok(S.is(t[0]));
+      assert.ok(S.is(t[1]));
+    });
+
+    it('should accept only valid values', function () {
+
+      throwsWithMessage(function () {
+        T(1);
+      }, '[tcomb] Invalid value 1 supplied to T (expected an array of length 2)');
+
+      throwsWithMessage(function () {
+        T([1, 1]);
+      }, '[tcomb] Invalid value 1 supplied to T/0: S (expected an object)');
+
+    });
+
+    it('should be idempotent', function () {
+      var T = t.tuple([t.String, t.Number]);
+      var p1 = T(['a', 1]);
+      var p2 = T(p1);
+      assert.deepEqual(Object.isFrozen(p1), true);
+      assert.deepEqual(Object.isFrozen(p2), true);
+      assert.deepEqual(p2 === p1, true);
+    });
+
+  });
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is an instance of the tuple', function () {
+      assert.ok(Area.is([1, 2]));
+    });
+
+    it('should return false when x is not an instance of the tuple', function () {
+      assert.strictEqual(Area.is([1]), false);
+      assert.strictEqual(Area.is([1, 2, 3]), false);
+      assert.strictEqual(Area.is([1, 'a']), false);
+    });
+
+    it('should not depend on `this`', function () {
+      assert.ok([[1, 2]].every(Area.is));
+    });
+
+  });
+
+  describe('#update()', function () {
+
+    var Type = t.tuple([t.String, t.Number]);
+    var instance = Type(['a', 1]);
+
+    it('should return a new instance', function () {
+      var newInstance = Type.update(instance, {0: {$set: 'b'}});
+      assert(Type.is(newInstance));
+      assert(instance[0] === 'a');
+      assert(newInstance[0] === 'b');
+    });
+
+  });
+
+});
diff --git a/test/union.js b/test/union.js
new file mode 100644
index 0000000..9f6fa9b
--- /dev/null
+++ b/test/union.js
@@ -0,0 +1,108 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+var Point = t.struct({
+  x: t.Number,
+  y: t.Number
+});
+
+describe('t.union(types, [name])', function () {
+
+  var Circle = t.struct({
+    center: Point,
+    radius: t.Number
+  }, 'Circle');
+
+  var Rectangle = t.struct({
+    a: Point,
+    b: Point
+  });
+
+  var Shape = t.union([Circle, Rectangle], 'Shape');
+
+  Shape.dispatch = function (values) {
+    assert(t.Object.is(values));
+    return values.hasOwnProperty('center') ?
+      Circle :
+      Rectangle;
+  };
+
+  describe('combinator', function () {
+
+    it('should throw if used with wrong arguments', function () {
+
+      throwsWithMessage(function () {
+        t.union();
+      }, '[tcomb] Invalid argument types undefined supplied to union(types, [name]) combinator (expected an array of at least 2 types)');
+
+      throwsWithMessage(function () {
+        t.union([]);
+      }, '[tcomb] Invalid argument types [] supplied to union(types, [name]) combinator (expected an array of at least 2 types)');
+
+      throwsWithMessage(function () {
+        t.union([1]);
+      }, '[tcomb] Invalid argument types [\n  1\n] supplied to union(types, [name]) combinator (expected an array of at least 2 types)');
+
+      throwsWithMessage(function () {
+        t.union([Circle, Point], 1);
+      }, '[tcomb] Invalid argument name 1 supplied to union(types, [name]) combinator (expected a string)');
+
+    });
+
+  });
+
+  describe('constructor', function () {
+
+    it('should have a default dispatch() implementation', function () {
+      var T = t.union([t.String, t.Number], 'T');
+      assert.deepEqual(T(1), 1);
+    });
+
+    it('should throw when dispatch() does not return a type', function () {
+      throwsWithMessage(function () {
+        var T = t.union([t.String, t.Number], 'T');
+        T(true);
+      }, '[tcomb] Invalid value true supplied to T');
+    });
+
+    it('should build instances when dispatch() is implemented', function () {
+      var circle = Shape({center: {x: 0, y: 0}, radius: 10});
+      assert.ok(Circle.is(circle));
+    });
+
+    it('should throw if used with new and union types are not instantiables with new', function () {
+      throwsWithMessage(function () {
+        var T = t.union([t.String, t.Number], 'T');
+        T.dispatch = function () { return t.String; };
+        var x = new T('a'); // eslint-disable-line
+      }, '[tcomb] Cannot use the new operator to instantiate the type T');
+    });
+
+    it('should not throw if used with new and union types are instantiables with new', function () {
+      assert.doesNotThrow(function () {
+        Shape({center: {x: 0, y: 0}, radius: 10});
+      });
+    });
+
+    it('should be idempotent', function () {
+      var p1 = Shape({center: {x: 0, y: 0}, radius: 10});
+      var p2 = Shape(p1);
+      assert.deepEqual(Object.isFrozen(p1), true);
+      assert.deepEqual(Object.isFrozen(p2), true);
+      assert.deepEqual(p2 === p1, true);
+    });
+
+  });
+
+  describe('#is(x)', function () {
+
+    it('should return true when x is an instance of the union', function () {
+      var p = new Circle({center: { x: 0, y: 0 }, radius: 10});
+      assert.ok(Shape.is(p));
+    });
+
+  });
+
+});
diff --git a/test/update.js b/test/update.js
new file mode 100644
index 0000000..ff07d7c
--- /dev/null
+++ b/test/update.js
@@ -0,0 +1,279 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+describe('t.update(instance, spec)', function () {
+
+  var update = t.update;
+  var Tuple = t.tuple([t.String, t.Number]);
+  var List = t.list(t.Number);
+  var Dict = t.dict(t.String, t.Number);
+
+  it('should throw if spec is invalid', function () {
+    throwsWithMessage(function () {
+      t.update({});
+    }, '[tcomb] Invalid argument spec undefined supplied to function update(instance, spec): expected an object containing commands');
+  });
+
+  it('should handle $set command', function () {
+    var instance = 1;
+    var actual = update(instance, {$set: 2});
+    assert.deepEqual(actual, 2);
+    instance = [1, 2, 3];
+    actual = update(instance, {1: {'$set': 4}});
+    assert.deepEqual(instance, [1, 2, 3]);
+    assert.deepEqual(actual, [1, 4, 3]);
+  });
+
+  it('$set and null value, fix #65', function () {
+    var NullStruct = t.struct({a: t.Number, b: t.maybe(t.Number)});
+    var instance = new NullStruct({a: 1});
+    var updated = update(instance, {b: {$set: 2}});
+    assert.deepEqual(instance, {a: 1, b: null});
+    assert.deepEqual(updated, {a: 1, b: 2});
+  });
+
+  it('should handle $apply command', function () {
+    var $apply = function (n) { return n + 1; };
+    var instance = 1;
+    var actual = update(instance, {$apply: $apply});
+    assert.deepEqual(actual, 2);
+    instance = [1, 2, 3];
+    actual = update(instance, {1: {'$apply': $apply}});
+    assert.deepEqual(instance, [1, 2, 3]);
+    assert.deepEqual(actual, [1, 3, 3]);
+  });
+
+  it('should handle $unshift command', function () {
+    var actual = update([1, 2, 3], {'$unshift': [4]});
+    assert.deepEqual(actual, [4, 1, 2, 3]);
+    actual = update([1, 2, 3], {'$unshift': [4, 5]});
+    assert.deepEqual(actual, [4, 5, 1, 2, 3]);
+    actual = update([1, 2, 3], {'$unshift': [[4, 5]]});
+    assert.deepEqual(actual, [[4, 5], 1, 2, 3]);
+  });
+
+  it('should handle $push command', function () {
+    var actual = update([1, 2, 3], {'$push': [4]});
+    assert.deepEqual(actual, [1, 2, 3, 4]);
+    actual = update([1, 2, 3], {'$push': [4, 5]});
+    assert.deepEqual(actual, [1, 2, 3, 4, 5]);
+    actual = update([1, 2, 3], {'$push': [[4, 5]]});
+    assert.deepEqual(actual, [1, 2, 3, [4, 5]]);
+  });
+
+  it('should handle $splice command', function () {
+    var instance = [1, 2, {a: [12, 17, 15]}];
+    var actual = update(instance, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
+    assert.deepEqual(instance, [1, 2, {a: [12, 17, 15]}]);
+    assert.deepEqual(actual, [1, 2, {a: [12, 13, 14, 15]}]);
+  });
+
+  it('should handle $remove command', function () {
+    var instance = {a: 1, b: 2};
+    var actual = update(instance, {'$remove': ['a']});
+    assert.deepEqual(instance, {a: 1, b: 2});
+    assert.deepEqual(actual, {b: 2});
+  });
+
+  it('should handle $swap command', function () {
+    var instance = [1, 2, 3, 4];
+    var actual = update(instance, {'$swap': {from: 1, to: 2}});
+    assert.deepEqual(instance, [1, 2, 3, 4]);
+    assert.deepEqual(actual, [1, 3, 2, 4]);
+  });
+
+  describe('structs', function () {
+
+    var Point = t.struct({
+      x: t.Number,
+      y: t.Number
+    });
+    var instance = new Point({x: 0, y: 1});
+
+    it('should handle $set command', function () {
+      var updated = update(instance, {x: {$set: 1}});
+      assert.deepEqual(instance, {x: 0, y: 1});
+      assert.deepEqual(updated, {x: 1, y: 1});
+    });
+
+    it('should handle $apply command', function () {
+      var updated = update(instance, {x: {$apply: function (x) {
+        return x + 2;
+      }}});
+      assert.deepEqual(instance, {x: 0, y: 1});
+      assert.deepEqual(updated, {x: 2, y: 1});
+    });
+
+    it('should handle $merge command', function () {
+      var updated = update(instance, {'$merge': {x: 2, y: 2}});
+      assert.deepEqual(instance, {x: 0, y: 1});
+      assert.deepEqual(updated, {x: 2, y: 2});
+      var Nested = t.struct({
+        a: t.Number,
+        b: t.struct({
+          c: t.Number,
+          d: t.Number,
+          e: t.Number
+        })
+      });
+      instance = new Nested({a: 1, b: {c: 2, d: 3, e: 4}});
+      updated = update(instance, {b: {'$merge': {c: 5, e: 6}}});
+      assert.deepEqual(instance, {a: 1, b: {c: 2, d: 3, e: 4}});
+      assert.deepEqual(updated, {a: 1, b: {c: 5, d: 3, e: 6}});
+    });
+
+  });
+
+  describe('tuples', function () {
+
+    var instance = Tuple(['a', 1]);
+
+    it('should handle $set command', function () {
+      var updated = update(instance, {0: {$set: 'b'}});
+      assert.deepEqual(updated, ['b', 1]);
+    });
+
+  });
+
+  describe('lists', function () {
+
+    var instance = List([1, 2, 3, 4]);
+
+    it('should handle $set command', function () {
+      var updated = update(instance, {2: {$set: 5}});
+      assert.deepEqual(updated, [1, 2, 5, 4]);
+    });
+
+    it('should handle $splice command', function () {
+      var updated = update(instance, {$splice: [[1, 2, 5, 6]]});
+      assert.deepEqual(updated, [1, 5, 6, 4]);
+    });
+
+    it('should handle $concat command', function () {
+      var updated = update(instance, {$push: [5]});
+      assert.deepEqual(updated, [1, 2, 3, 4, 5]);
+      updated = update(instance, {$push: [5, 6]});
+      assert.deepEqual(updated, [1, 2, 3, 4, 5, 6]);
+    });
+
+    it('should handle $prepend command', function () {
+      var updated = update(instance, {$unshift: [5]});
+      assert.deepEqual(updated, [5, 1, 2, 3, 4]);
+      updated = update(instance, {$unshift: [5, 6]});
+      assert.deepEqual(updated, [5, 6, 1, 2, 3, 4]);
+    });
+
+    it('should handle $swap command', function () {
+      var updated = update(instance, {$swap: {from: 1, to: 2}});
+      assert.deepEqual(updated, [1, 3, 2, 4]);
+    });
+
+  });
+
+  describe('dicts', function () {
+
+    var instance = Dict({a: 1, b: 2});
+
+    it('should handle $set command', function () {
+      var updated = update(instance, {a: {$set: 2}});
+      assert.deepEqual(updated, {a: 2, b: 2});
+      updated = update(instance, {c: {$set: 3}});
+      assert.deepEqual(updated, {a: 1, b: 2, c: 3});
+    });
+
+    it('should handle $remove command', function () {
+      var updated = update(instance, {$remove: ['a']});
+      assert.deepEqual(updated, {b: 2});
+    });
+
+  });
+
+  describe('memory saving', function () {
+
+    it('should reuse members that are not updated', function () {
+      var Struct = t.struct({
+        a: t.Number,
+        b: t.String,
+        c: t.tuple([t.Number, t.Number])
+      });
+      var List = t.list(Struct);
+      var instance = List([{
+        a: 1,
+        b: 'one',
+        c: [1000, 1000000]
+      }, {
+        a: 2,
+        b: 'two',
+        c: [2000, 2000000]
+      }]);
+
+      var updated = update(instance, {
+        1: {
+          a: {$set: 119}
+        }
+      });
+
+      assert.strictEqual(updated[0], instance[0]);
+      assert.notStrictEqual(updated[1], instance[1]);
+      assert.strictEqual(updated[1].c, instance[1].c);
+    });
+  });
+
+  describe('all together now', function () {
+
+    it('should handle mixed commands', function () {
+      var Struct = t.struct({
+        a: t.Number,
+        b: Tuple,
+        c: List,
+        d: Dict
+      });
+      var instance = new Struct({
+        a: 1,
+        b: ['a', 1],
+        c: [1, 2, 3, 4],
+        d: {a: 1, b: 2}
+      });
+      var updated = update(instance, {
+        a: {$set: 1},
+        b: {0: {$set: 'b'}},
+        c: {2: {$set: 5}},
+        d: {$remove: ['a']}
+      });
+      assert.deepEqual(updated, {
+        a: 1,
+        b: ['b', 1],
+        c: [1, 2, 5, 4],
+        d: {b: 2}
+      });
+    });
+
+    it('should handle nested structures', function () {
+      var Struct = t.struct({
+        a: t.struct({
+          b: t.tuple([
+            t.String,
+            t.list(t.Number)
+          ])
+        })
+      });
+      var instance = new Struct({
+        a: {
+          b: ['a', [1, 2, 3]]
+        }
+      });
+      var updated = update(instance, {
+        a: {b: {1: {2: {$set: 4}}}}
+      });
+      assert.deepEqual(updated, {
+        a: {
+          b: ['a', [1, 2, 4]]
+        }
+      });
+    });
+
+  });
+
+});
diff --git a/test/util.js b/test/util.js
new file mode 100644
index 0000000..fece228
--- /dev/null
+++ b/test/util.js
@@ -0,0 +1,13 @@
+var assert = require('assert');
+
+function throwsWithMessage(f, message) {
+  assert.throws(f, function (err) {
+    assert.strictEqual(err instanceof Error, true);
+    assert.strictEqual(err.message, message);
+    return true;
+  });
+}
+
+module.exports = {
+  throwsWithMessage: throwsWithMessage
+};