Repository: gcanti/tcomb
Pull request #148 (2015-09-22 08:02:24)

v2.5


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2de8b19..5f627a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,15 @@
 **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.5.0
+
+- **New Feature**
+  - check if the type returned by a union dispatch function is correct, fix #136 (thanks @fcracker79)
+  - added `refinement` alias to `subtype` (which is deprecated), fix #140
+- **Internal**
+  - optimisations: for identity types return early in production, fix #135 (thanks @fcracker79)
+  - exposed `getDefaultName` on combinator constructors
+
 ## v2.4.1
 
 - **New Feature**
diff --git a/GUIDE.md b/GUIDE.md
index 46f6de7..d5fa34b 100644
--- a/GUIDE.md
+++ b/GUIDE.md
@@ -24,22 +24,22 @@ An *irreducible* type is a type that can't be built with other types. Examples o
 
 **JavaScript native types**
 
-* `t.String`: strings (short alias `t.Str`)
-* `t.Number`: numbers (short alias `t.Num`)
-* `t.Boolean`: booleans (short alias `t.Bool`)
-* `t.Array`: arrays (short alias `t.Arr`)
-* `t.Object`: plain objects (short alias `t.Obj`)
-* `t.Function`: functions (short alias `t.Func`)
-* `t.Error`: errors (short alias `t.Err`)
-* `t.RegExp`: regular expressions (short alias `t.Re`)
-* `t.Date`: dates (short alias `t.Dat`)
+* `t.String`: strings (deprecated alias `t.Str`)
+* `t.Number`: numbers (deprecated alias `t.Num`)
+* `t.Boolean`: booleans (deprecated alias `t.Bool`)
+* `t.Array`: arrays (deprecated alias `t.Arr`)
+* `t.Object`: plain objects (deprecated alias `t.Obj`)
+* `t.Function`: functions (deprecated alias `t.Func`)
+* `t.Error`: errors (deprecated alias `t.Err`)
+* `t.RegExp`: regular expressions (deprecated alias `t.Re`)
+* `t.Date`: dates (deprecated alias `t.Dat`)
 
 There are 2 additional irriducible types defined in tcomb:
 
 **Additional types**
 
 * `t.Nil`: `null` or `undefined`
-* `t.Any`: any type
+* `t.Any`: any value
 
 ## Type checking with the `is` function
 
@@ -145,9 +145,9 @@ Example: the `meta` object of `t.String`:
 
 *Type combinators* are the tcomb way to define new composite types from those already defined, that is they **combine** old types in a new one.
 
-## The subtype combinator
+## The refinement combinator (deprecated alias `subtype`)
 
-You can refine a type using the `subtype(type, predicate, name)` combinator where:
+You can refine a type using the `refinement(type, predicate, name)` combinator where:
 
 * `type` is a type already defined
 * `predicate` is a predicate
@@ -157,13 +157,13 @@ Example:
 
 ```js
 // defines a type representing positive numbers
-var Positive = t.subtype(t.Number, (n) => n >= 0, 'Positive');
+var Positive = t.refinement(t.Number, (n) => n >= 0, 'Positive');
 
 Positive.is(1);  // => true
 Positive.is(-1); // => false
 ```
 
-Subtypes have the following `meta` object:
+Refinements have the following `meta` object:
 
 ```js
 {
@@ -438,8 +438,8 @@ You can define an intersection of types using the `intersection(types, name)` co
 * `name` is an optional string useful for debugging purposes
 
 ```js
-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 Min = t.refinement(t.String, function (s) { return s.length > 2; }, 'Min');
+var Max = t.refinement(t.String, function (s) { return s.length < 5; }, 'Max');
 var MinMax = t.intersection([Min, Max], 'MinMax');
 
 MinMax.is('abc'); // => true
@@ -525,7 +525,7 @@ The domain and codomain can also be specified using types from any combinator in
 var B = t.func(t.func(t.String, t.Number), t.String);
 
 // An `ExcitedString` is a `t.String` containing an exclamation mark
-var ExcitedString = t.subtype(t.String, function (s) { return s.indexOf('!') !== -1; }, 'ExcitedString');
+var ExcitedString = t.refinement(t.String, function (s) { return s.indexOf('!') !== -1; }, 'ExcitedString');
 
 // An `Exciter` takes a `t.String` and returns an `ExcitedString`
 var Exciter = t.func(t.String, ExcitedString);
@@ -563,7 +563,7 @@ var simpleQuestionator = Exciter.of(function (s) { return s + '?'; });
 var simpleExciter      = Exciter.of(function (s) { return s + '!'; });
 
 // Raises error:
-// Invalid `Hello?` supplied to `ExcitedString`, insert a valid value for the subtype
+// Invalid `Hello?` supplied to `ExcitedString`, insert a valid value for the refinement
 simpleQuestionator('Hello');
 
 // Raises error: Invalid `1` supplied to `String`
diff --git a/README.md b/README.md
index 122a4b2..b055008 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ tcomb is a library for Node.js and the browser which allows you to **check the t
 var t = require('tcomb');
 
 // a user defined type
-var Integer = t.subtype(t.Number, function (n) { return n % 1 === 0; }, 'Integer');  // <= give a name for better debug messages
+var Integer = t.refinement(t.Number, function (n) { return n % 1 === 0; }, 'Integer');  // <= give a name for better debug messages
 
 // a struct
 var Person = t.struct({
@@ -51,7 +51,7 @@ Write complex domain models in a breeze and with a small code footprint. Support
 * structs
 * lists
 * enums
-* subtypes
+* refinements
 * unions
 * intersections
 * the option type
diff --git a/index.js b/index.js
index 66af09a..6e478c4 100644
--- a/index.js
+++ b/index.js
@@ -2,25 +2,22 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (c) 2014 Giulio Canti
+ * Copyright (c) 2014-2015 Giulio Canti
  *
  */
 
 'use strict';
 
-// configurable
+// overrideable by the user
 exports.stringify = function stringify(x) {
   try { // handle "Converting circular structure to JSON" error
     return JSON.stringify(x, null, 2);
-  } catch (e) {
+  }
+  catch (e) {
     return String(x);
   }
 };
 
-function isInstanceOf(x, constructor) {
-  return x instanceof constructor;
-}
-
 function isNil(x) {
   return x === null || x === void 0;
 }
@@ -42,7 +39,7 @@ function isFunction(x) {
 }
 
 function isArray(x) {
-  return isInstanceOf(x, Array);
+  return x instanceof Array;
 }
 
 function isObject(x) {
@@ -54,26 +51,38 @@ function isType(x) {
 }
 
 function isStruct(x) {
-  return isType(x) && (x.meta.kind === 'struct');
+  return isType(x) && ( x.meta.kind === 'struct' );
 }
 
 function isMaybe(x) {
-  return isType(x) && (x.meta.kind === 'maybe');
+  return isType(x) && ( x.meta.kind === 'maybe' );
 }
 
 function isUnion(x) {
-  return isType(x) && (x.meta.kind === 'union');
+  return isType(x) && ( x.meta.kind === 'union' );
 }
 
 function isTypeName(name) {
   return isNil(name) || isString(name);
 }
 
-// Returns true if x is of type `type`
+// returns true if x is an instance of type
 function is(x, type) {
-  return isType(type) ? type.is(x) : isInstanceOf(x, type); // type should be a class constructor
+  if (isType(type)) {
+    return type.is(x);
+  }
+  return x instanceof type; // type should be a class constructor
+}
+
+// return true if the type constructor behaves like the identity function (exceptions are the structs)
+function isIdentity(type) {
+  if (isType(type)) {
+    return type.meta.identity;
+  }
+  return true; // ES6 classes are identity for tcomb
 }
 
+// creates an instance of a type, handling the optional new operator
 function create(type, value, path) {
   if (isType(type)) {
     // for structs the new operator is allowed
@@ -81,9 +90,9 @@ function create(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
+    // here type should be a class constructor and value some instance, just check membership and return the value
     path = path || [getFunctionName(type)];
-    assert(isInstanceOf(value, type), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/'); });
+    assert(value instanceof type, function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/'); });
   }
 
   return value;
@@ -94,28 +103,30 @@ function getFunctionName(f) {
 }
 
 function getTypeName(constructor) {
-  if (isType(constructor)) { return constructor.displayName; }
+  if (isType(constructor)) {
+    return constructor.displayName;
+  }
   return getFunctionName(constructor);
 }
 
-// configurable
+// overrideable by the user
 exports.fail = function fail(message) {
   throw new TypeError('[tcomb] ' + message);
 };
 
 function assert(guard, message) {
   if (guard !== true) {
-    if (isFunction(message)) {
+    if (isFunction(message)) { // handle lazy messages
       message = message();
     }
-    else if (isNil(message)) {
+    else if (isNil(message)) { // use a default message
       message = 'Assert failed (turn on "Pause on exceptions" in your Source panel)';
     }
     exports.fail(message);
   }
 }
 
-// safe mixin: cannot override props unless specified
+// safe mixin, cannot override props unless specified
 function mixin(target, source, overwrite) {
   if (isNil(source)) { return target; }
   for (var k in source) {
@@ -132,12 +143,16 @@ function mixin(target, source, overwrite) {
 }
 
 function forbidNewOperator(x, type) {
-  assert(!isInstanceOf(x, type), function () { return 'Cannot use the new operator to instantiate the type ' + getTypeName(type); });
+  assert(!(x instanceof type), function () { return 'Cannot use the new operator to instantiate the type ' + getTypeName(type); });
 }
 
 function getShallowCopy(x) {
-  if (isArray(x)) { return x.concat(); }
-  if (isObject(x)) { return mixin({}, x); }
+  if (isArray(x)) {
+    return x.concat();
+  }
+  if (isObject(x)) {
+    return mixin({}, x);
+  }
   return x;
 }
 
@@ -261,7 +276,8 @@ function irreducible(name, predicate) {
 
   Irreducible.meta = {
     kind: 'irreducible',
-    name: name
+    name: name,
+    identity: true
   };
 
   Irreducible.displayName = name;
@@ -290,23 +306,17 @@ var Obj = irreducible('Object', isObject);
 var Func = irreducible('Function', isFunction);
 
 var Err = irreducible('Error', function (x) {
-  return isInstanceOf(x, Error);
+  return x instanceof Error;
 });
 
 var Re = irreducible('RegExp', function (x) {
-  return isInstanceOf(x, RegExp);
+  return x instanceof RegExp;
 });
 
 var Dat = irreducible('Date', function (x) {
-  return isInstanceOf(x, Date);
+  return x instanceof Date;
 });
 
-function getDefaultStructName(props) {
-  return '{' + Object.keys(props).map(function (prop) {
-    return prop + ': ' + getTypeName(props[prop]);
-  }).join(', ') + '}';
-}
-
 function struct(props, name) {
 
   if (process.env.NODE_ENV !== 'production') {
@@ -314,11 +324,11 @@ function struct(props, name) {
     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);
+  var displayName = name || struct.getDefaultName(props);
 
   function Struct(value, path) {
 
-    if (Struct.is(value)) { // makes Struct idempotent
+    if (Struct.is(value)) { // implements idempotency
       return value;
     }
 
@@ -327,7 +337,7 @@ function struct(props, name) {
       assert(isObject(value), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/') + ' (expected an object)'; });
     }
 
-    if (!isInstanceOf(this, Struct)) { // makes `new` optional
+    if (!(this instanceof Struct)) { // `new` is optional
       return new Struct(value);
     }
 
@@ -348,13 +358,14 @@ function struct(props, name) {
   Struct.meta = {
     kind: 'struct',
     props: props,
-    name: name
+    name: name,
+    identity: false
   };
 
   Struct.displayName = displayName;
 
   Struct.is = function (x) {
-    return isInstanceOf(x, Struct);
+    return x instanceof Struct;
   };
 
   Struct.update = function (instance, spec) {
@@ -384,9 +395,11 @@ function struct(props, name) {
   return Struct;
 }
 
-function getDefaultUnionName(types) {
-  return types.map(getTypeName).join(' | ');
-}
+struct.getDefaultName = function (props) {
+  return '{' + Object.keys(props).map(function (prop) {
+    return prop + ': ' + getTypeName(props[prop]);
+  }).join(', ') + '}';
+};
 
 function union(types, name) {
 
@@ -395,19 +408,24 @@ function union(types, name) {
     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);
+  var displayName = name || union.getDefaultName(types);
+  var identity = types.every(isIdentity);
 
   function Union(value, path) {
 
-    if (process.env.NODE_ENV !== 'production') {
-      forbidNewOperator(this, Union);
+    if (process.env.NODE_ENV === 'production') {
+      if (identity) {
+        return value;
+      }
     }
 
     var type = Union.dispatch(value);
 
     if (process.env.NODE_ENV !== 'production') {
+      forbidNewOperator(this, Union);
       path = path || [displayName];
-      assert(isType(type), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/'); });
+      assert(isType(type), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/') + ' (no constructor found)'; });
+      assert(types.some(function (t) { return t === type; }), function () { return 'Invalid constructor ' + getTypeName(type) + ' returned by ' + path.join('/') + '.dispatch(x) function'; });
       path[path.length - 1] += '(' + getTypeName(type) + ')';
     }
 
@@ -417,7 +435,8 @@ function union(types, name) {
   Union.meta = {
     kind: 'union',
     types: types,
-    name: name
+    name: name,
+    identity: identity
   };
 
   Union.displayName = displayName;
@@ -431,7 +450,7 @@ function union(types, name) {
   Union.dispatch = function (x) { // default dispatch implementation
     for (var i = 0, len = types.length; i < len; i++ ) {
       var type = types[i];
-      if (isUnion(type)) {
+      if (isUnion(type)) { // handle union of unions
         var t = type.dispatch(x);
         if (!isNil(t)) {
           return t;
@@ -450,9 +469,9 @@ function union(types, name) {
   return Union;
 }
 
-function getDefaultIntersectionName(types) {
-  return types.map(getTypeName).join(' & ');
-}
+union.getDefaultName = function (types) {
+  return types.map(getTypeName).join(' | ');
+};
 
 function intersection(types, name) {
 
@@ -461,7 +480,7 @@ function intersection(types, name) {
     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);
+  var displayName = name || intersection.getDefaultName(types);
 
   function Intersection(value, path) {
 
@@ -477,7 +496,8 @@ function intersection(types, name) {
   Intersection.meta = {
     kind: 'intersection',
     types: types,
-    name: name
+    name: name,
+    identity: true
   };
 
   Intersection.displayName = displayName;
@@ -495,25 +515,22 @@ function intersection(types, name) {
   return Intersection;
 }
 
-function getDefaultMaybeName(type) {
-  return '?' + getTypeName(type);
-}
+intersection.getDefaultName = function (types) {
+  return types.map(getTypeName).join(' & ');
+};
 
 function maybe(type, name) {
 
-  if (process.env.NODE_ENV !== 'production') {
-    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
     return type;
   }
 
   if (process.env.NODE_ENV !== 'production') {
+    assert(isFunction(type), function () { return 'Invalid argument type ' + exports.stringify(type) + ' supplied to maybe(type, [name]) combinator (expected a type)'; });
     assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to maybe(type, [name]) combinator (expected a string)'; });
   }
 
-  var displayName = name || getDefaultMaybeName(type);
+  var displayName = name || maybe.getDefaultName(type);
 
   function Maybe(value, path) {
     if (process.env.NODE_ENV !== 'production') {
@@ -525,7 +542,8 @@ function maybe(type, name) {
   Maybe.meta = {
     kind: 'maybe',
     type: type,
-    name: name
+    name: name,
+    identity: isIdentity(type)
   };
 
   Maybe.displayName = displayName;
@@ -537,9 +555,9 @@ function maybe(type, name) {
   return Maybe;
 }
 
-function getDefaultEnumsName(map) {
-  return Object.keys(map).map(function (k) { return exports.stringify(k); }).join(' | ');
-}
+maybe.getDefaultName = function (type) {
+  return '?' + getTypeName(type);
+};
 
 function enums(map, name) {
 
@@ -548,7 +566,7 @@ function enums(map, name) {
     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);
+  var displayName = name || enums.getDefaultName(map);
 
   function Enums(value, path) {
 
@@ -564,7 +582,8 @@ function enums(map, name) {
   Enums.meta = {
     kind: 'enums',
     map: map,
-    name: name
+    name: name,
+    identity: true
   };
 
   Enums.displayName = displayName;
@@ -576,6 +595,10 @@ function enums(map, name) {
   return Enums;
 }
 
+enums.getDefaultName = function (map) {
+  return Object.keys(map).map(function (k) { return exports.stringify(k); }).join(' | ');
+};
+
 enums.of = function (keys, name) {
   keys = isString(keys) ? keys.split(' ') : keys;
   var value = {};
@@ -585,10 +608,6 @@ enums.of = function (keys, name) {
   return enums(value, name);
 };
 
-function getDefaultTupleName(types) {
-  return '[' + types.map(getTypeName).join(', ') + ']';
-}
-
 function tuple(types, name) {
 
   if (process.env.NODE_ENV !== 'production') {
@@ -596,46 +615,48 @@ function tuple(types, name) {
     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);
-
-  function isTuple(x) {
-    return types.every(function (type, i) {
-      return is(x[i], type);
-    });
-  }
+  var displayName = name || tuple.getDefaultName(types);
+  var identity = types.every(isIdentity);
 
   function Tuple(value, path) {
 
+    if (process.env.NODE_ENV === 'production') {
+      if (identity) {
+        return value;
+      }
+    }
+
     if (process.env.NODE_ENV !== 'production') {
       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
-      if (process.env.NODE_ENV !== 'production') {
-        Object.freeze(value);
-      }
-      return value;
+    var idempotent = true;
+    var ret = [];
+    for (var i = 0, len = types.length; i < len; i++) {
+      var expected = types[i];
+      var actual = value[i];
+      var instance = create(expected, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(i + ': ' + getTypeName(expected)) : null ));
+      idempotent = idempotent && ( actual === instance );
+      ret.push(instance);
     }
 
-    var arr = [], expected, actual;
-    for (var i = 0, len = types.length; i < len; i++) {
-      expected = types[i];
-      actual = value[i];
-      arr.push(create(expected, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(i + ': ' + getTypeName(expected)) : null )));
+    if (idempotent) { // implements idempotency
+      ret = value;
     }
 
     if (process.env.NODE_ENV !== 'production') {
-      Object.freeze(arr);
+      Object.freeze(ret);
     }
 
-    return arr;
+    return ret;
   }
 
   Tuple.meta = {
     kind: 'tuple',
     types: types,
-    name: name
+    name: name,
+    identity: identity
   };
 
   Tuple.displayName = displayName;
@@ -643,7 +664,9 @@ function tuple(types, name) {
   Tuple.is = function (x) {
     return isArray(x) &&
       x.length === types.length &&
-      isTuple(x);
+      types.every(function (type, i) {
+        return is(x[i], type);
+      });
   };
 
   Tuple.update = function (instance, spec) {
@@ -653,24 +676,25 @@ function tuple(types, name) {
   return Tuple;
 }
 
-function getDefaultSubtypeName(type, predicate) {
-  return '{' + getTypeName(type) + ' | ' + getFunctionName(predicate) + '}';
-}
+tuple.getDefaultName = function (types) {
+  return '[' + types.map(getTypeName).join(', ') + ']';
+};
 
-function subtype(type, predicate, name) {
+function refinement(type, predicate, name) {
 
   if (process.env.NODE_ENV !== 'production') {
-    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)'; });
+    assert(isFunction(type), function () { return 'Invalid argument type ' + exports.stringify(type) + ' supplied to refinement(type, predicate, [name]) combinator (expected a type)'; });
+    assert(isFunction(predicate), function () { return 'Invalid argument predicate supplied to refinement(type, predicate, [name]) combinator (expected a function)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + exports.stringify(name) + ' supplied to refinement(type, predicate, [name]) combinator (expected a string)'; });
   }
 
-  var displayName = name || getDefaultSubtypeName(type, predicate);
+  var displayName = name || refinement.getDefaultName(type, predicate);
+  var identity = isIdentity(type);
 
-  function Subtype(value, path) {
+  function Refinement(value, path) {
 
     if (process.env.NODE_ENV !== 'production') {
-      forbidNewOperator(this, Subtype);
+      forbidNewOperator(this, Refinement);
       path = path || [displayName];
     }
 
@@ -683,29 +707,30 @@ function subtype(type, predicate, name) {
     return x;
   }
 
-  Subtype.meta = {
+  Refinement.meta = {
     kind: 'subtype',
     type: type,
     predicate: predicate,
-    name: name
+    name: name,
+    identity: identity
   };
 
-  Subtype.displayName = displayName;
+  Refinement.displayName = displayName;
 
-  Subtype.is = function (x) {
+  Refinement.is = function (x) {
     return is(x, type) && predicate(x);
   };
 
-  Subtype.update = function (instance, spec) {
-    return Subtype(exports.update(instance, spec));
+  Refinement.update = function (instance, spec) {
+    return Refinement(exports.update(instance, spec));
   };
 
-  return Subtype;
+  return Refinement;
 }
 
-function getDefaultListName(type) {
-  return 'Array<' + getTypeName(type) + '>';
-}
+refinement.getDefaultName = function (type, predicate) {
+  return '{' + getTypeName(type) + ' | ' + getFunctionName(predicate) + '}';
+};
 
 function list(type, name) {
 
@@ -714,52 +739,56 @@ function list(type, name) {
     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 displayName = name || list.getDefaultName(type);
   var typeNameCache = getTypeName(type);
-
-  function isList(x) {
-    return x.every(function (e) {
-      return is(e, type);
-    });
-  }
+  var identity = isIdentity(type); // the list is identity iif type is identity
 
   function List(value, path) {
 
+    if (process.env.NODE_ENV === 'production') {
+      if (identity) {
+        return value; // just trust the input if elements must not be hydrated
+      }
+    }
+
     if (process.env.NODE_ENV !== 'production') {
       path = path || [displayName];
       assert(isArray(value), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/') + ' (expected an array of ' + typeNameCache + ')'; });
     }
 
-    if (isList(value)) { // makes List idempotent
-      if (process.env.NODE_ENV !== 'production') {
-        Object.freeze(value);
-      }
-      return value;
-    }
-
-    var arr = [];
+    var idempotent = true; // will remain true if I can reutilise the input
+    var ret = []; // make a temporary copy, will be discarded if idempotent remains true
     for (var i = 0, len = value.length; i < len; i++ ) {
       var actual = value[i];
-      arr.push(create(type, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(i + ': ' + typeNameCache) : null )));
+      var instance = create(type, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(i + ': ' + typeNameCache) : null ));
+      idempotent = idempotent && ( actual === instance );
+      ret.push(instance);
+    }
+
+    if (idempotent) { // implements idempotency
+      ret = value;
     }
 
     if (process.env.NODE_ENV !== 'production') {
-      Object.freeze(arr);
+      Object.freeze(ret);
     }
 
-    return arr;
+    return ret;
   }
 
   List.meta = {
     kind: 'list',
     type: type,
-    name: name
+    name: name,
+    identity: identity
   };
 
   List.displayName = displayName;
 
   List.is = function (x) {
-    return isArray(x) && isList(x);
+    return isArray(x) && x.every(function (e) {
+      return is(e, type);
+    });
   };
 
   List.update = function (instance, spec) {
@@ -769,9 +798,9 @@ function list(type, name) {
   return List;
 }
 
-function getDefaultDictName(domain, codomain) {
-  return '{[key: ' + getTypeName(domain) + ']: ' + getTypeName(codomain) + '}';
-}
+list.getDefaultName = function (type) {
+  return 'Array<' + getTypeName(type) + '>';
+};
 
 function dict(domain, codomain, name) {
 
@@ -781,62 +810,69 @@ function dict(domain, codomain, name) {
     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 displayName = name || dict.getDefaultName(domain, codomain);
   var domainNameCache = getTypeName(domain);
   var codomainNameCache = getTypeName(codomain);
+  var identity = isIdentity(domain) && isIdentity(codomain);
 
-  function isDict(x) {
-    for (var k in x) {
-      if (x.hasOwnProperty(k)) {
-        if (!is(k, domain) || !is(x[k], codomain)) {
-          return false;
-        }
+  function Dict(value, path) {
+
+    if (process.env.NODE_ENV === 'production') {
+      if (identity) {
+        return value; // just trust the input if elements must not be hydrated
       }
     }
-    return true;
-  }
-
-  function Dict(value, path) {
 
     if (process.env.NODE_ENV !== 'production') {
       path = path || [displayName];
       assert(isObject(value), function () { return 'Invalid value ' + exports.stringify(value) + ' supplied to ' + path.join('/'); });
     }
 
-    if (isDict(value)) { // makes Dict idempotent
-      if (process.env.NODE_ENV !== 'production') {
-        Object.freeze(value);
-      }
-      return value;
-    }
-
-    var obj = {};
+    var idempotent = true; // will remain true if I can reutilise the input
+    var ret = {}; // make a temporary copy, will be discarded if idempotent remains true
     for (var k in value) {
       if (value.hasOwnProperty(k)) {
         k = create(domain, k, ( process.env.NODE_ENV !== 'production' ? path.concat(domainNameCache) : null ));
         var actual = value[k];
-        obj[k] = create(codomain, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(k + ': ' + codomainNameCache) : null ));
+        var instance = create(codomain, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(k + ': ' + codomainNameCache) : null ));
+        idempotent = idempotent && ( actual === instance );
+        ret[k] = instance;
       }
     }
 
+    if (idempotent) { // implements idempotency
+      ret = value;
+    }
+
     if (process.env.NODE_ENV !== 'production') {
-      Object.freeze(obj);
+      Object.freeze(ret);
     }
 
-    return obj;
+    return ret;
   }
 
   Dict.meta = {
     kind: 'dict',
     domain: domain,
     codomain: codomain,
-    name: name
+    name: name,
+    identity: identity
   };
 
   Dict.displayName = displayName;
 
   Dict.is = function (x) {
-    return isObject(x) && isDict(x);
+    if (!isObject(x)) {
+      return false;
+    }
+    for (var k in x) {
+      if (x.hasOwnProperty(k)) {
+        if (!is(k, domain) || !is(x[k], codomain)) {
+          return false;
+        }
+      }
+    }
+    return true;
   };
 
   Dict.update = function (instance, spec) {
@@ -846,14 +882,14 @@ function dict(domain, codomain, name) {
   return Dict;
 }
 
+dict.getDefaultName = function (domain, codomain) {
+  return '{[key: ' + getTypeName(domain) + ']: ' + getTypeName(codomain) + '}';
+};
+
 function isInstrumented(f) {
   return isFunction(f) && isObject(f.instrumentation);
 }
 
-function getDefaultFuncName(domain, codomain) {
-  return '(' + domain.map(getTypeName).join(', ') + ') => ' + getTypeName(codomain);
-}
-
 function func(domain, codomain, name) {
 
   domain = isArray(domain) ? domain : [domain]; // handle handy syntax for unary functions
@@ -864,12 +900,12 @@ function func(domain, codomain, name) {
     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);
+  var displayName = name || func.getDefaultName(domain, codomain);
 
-  function FuncType(value, uncurried) {
+  function FuncType(value, curried) {
 
     if (!isInstrumented(value)) { // automatically instrument the function
-      return FuncType.of(value, uncurried);
+      return FuncType.of(value, curried);
     }
 
     if (process.env.NODE_ENV !== 'production') {
@@ -943,6 +979,10 @@ function func(domain, codomain, name) {
 
 }
 
+func.getDefaultName = function (domain, codomain) {
+  return '(' + domain.map(getTypeName).join(', ') + ') => ' + getTypeName(codomain);
+};
+
 function match(x) {
   var type, guard, f, count;
   for (var i = 1, len = arguments.length; i < len; ) {
@@ -982,23 +1022,23 @@ mixin(exports, {
   assert: assert,
   Any: Any,
   Nil: Nil,
-  Str: Str,
+  Str: Str, // deprecated
   String: Str,
-  Num: Num,
+  Num: Num, // deprecated
   Number: Num,
-  Bool: Bool,
+  Bool: Bool, // deprecated
   Boolean: Bool,
-  Arr: Arr,
+  Arr: Arr, // deprecated
   Array: Arr,
-  Obj: Obj,
+  Obj: Obj, // deprecated
   Object: Obj,
-  Func: Func,
+  Func: Func, // deprecated
   Function: Func,
-  Err: Err,
+  Err: Err, // deprecated
   Error: Err,
-  Re: Re,
+  Re: Re, // deprecated
   RegExp: Re,
-  Dat: Dat,
+  Dat: Dat, // deprecated
   Date: Dat,
   irreducible: irreducible,
   struct: struct,
@@ -1006,7 +1046,8 @@ mixin(exports, {
   union: union,
   maybe: maybe,
   tuple: tuple,
-  subtype: subtype,
+  subtype: refinement, // deprecated
+  refinement: refinement,
   list: list,
   dict: dict,
   func: func,
diff --git a/package.json b/package.json
index fc21cd9..07dfb20 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "tcomb",
-  "version": "2.4.1",
+  "version": "2.5.0",
   "description": "Type checking and DDD for JavaScript",
   "main": "index.js",
   "scripts": {


Test patch:
diff --git a/test/dict.js b/test/dict.js
index afd5359..30ca082 100644
--- a/test/dict.js
+++ b/test/dict.js
@@ -1,12 +1,7 @@
 /* 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
-});
+var util = require('./util');
 
 describe('t.dict(domain, codomain, [name])', function () {
 
@@ -14,16 +9,16 @@ describe('t.dict(domain, codomain, [name])', function () {
 
     it('should throw if used with wrong arguments', function () {
 
-      throwsWithMessage(function () {
+      util.throwsWithMessage(function () {
         t.dict();
       }, '[tcomb] Invalid argument domain undefined supplied to dict(domain, codomain, [name]) combinator (expected a type)');
 
-      throwsWithMessage(function () {
+      util.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);
+      util.throwsWithMessage(function () {
+        t.dict(t.String, t.String, 1);
       }, '[tcomb] Invalid argument name 1 supplied to dict(domain, codomain, [name]) combinator (expected a string)');
 
     });
@@ -32,70 +27,97 @@ describe('t.dict(domain, codomain, [name])', function () {
 
   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');
+    var Codomain = t.struct({name: t.String}, 'Codomain');
+    var Dictionary = t.dict(Domain, Codomain, 'Dictionary');
 
-    it('should coerce values', function () {
-      var t = T({a: {}});
-      assert.ok(S.is(t.a));
-    });
+    it('should throw with a contextual error message if used with wrong arguments', function () {
 
-    it('should accept only valid values', function () {
+      util.throwsWithMessage(function () {
+        Dictionary(1);
+      }, '[tcomb] Invalid value 1 supplied to Dictionary');
 
-      throwsWithMessage(function () {
-        T(1);
-      }, '[tcomb] Invalid value 1 supplied to T');
+      util.throwsWithMessage(function () {
+        Dictionary({a: 1});
+      }, '[tcomb] Invalid value 1 supplied to Dictionary/a: Codomain (expected an object)');
 
-      throwsWithMessage(function () {
-        T({a: 1});
-      }, '[tcomb] Invalid value 1 supplied to T/a: S (expected an object)');
+      util.throwsWithMessage(function () {
+        Dictionary({forbidden: {}});
+      }, '[tcomb] Invalid value "forbidden" supplied to Dictionary/Domain');
 
-      throwsWithMessage(function () {
-        T({forbidden: {}});
-      }, '[tcomb] Invalid value "forbidden" supplied to T/Domain');
+    });
 
+    it('should hydrate the values of the dictionary', function () {
+      var instance = Dictionary({a: {name: 'Giulio'}});
+      assert.ok(Codomain.is(instance.a));
     });
 
+    it('should hydrate the values of the dictionary in production', util.production(function () {
+      var instance = Dictionary({a: {name: 'Giulio'}});
+      assert.ok(Codomain.is(instance.a));
+    }));
+
     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);
+      var d0 = {a: {name: 'Giulio'}, b: {name: 'Guido'}};
+      var d1 = Dictionary(d0);
+      var d2 = Dictionary(d1);
+      assert.equal(d0 === d1, false);
+      assert.equal(d1 === d2, true);
     });
 
+    it('should be idempotent in production', util.production(function () {
+      var d0 = {a: {name: 'Giulio'}, b: {name: 'Guido'}};
+      var d1 = Dictionary(d0);
+      var d2 = Dictionary(d1);
+      assert.equal(d0 === d1, false);
+      assert.equal(d1 === d2, true);
+    }));
+
+    it('should freeze the instance', function () {
+      var instance = Dictionary({});
+      assert.equal(Object.isFrozen(instance), true);
+    });
+
+    it('should not freeze the instance in production', util.production(function () {
+      var instance = Dictionary({});
+      assert.equal(Object.isFrozen(instance), false);
+    }));
+
   });
 
-  describe('#is(x)', function () {
+  describe('is(x)', function () {
+
+    var Point = t.struct({
+      x: t.Number,
+      y: t.Number
+    });
 
     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 return true when x is a dictionary', function () {
+      assert.equal(T.is({a: p1, b: p2}), true);
+      assert.equal(T.is({a: {x: 0, y: 0}, b: {x: 1, y: 1}}), false);
     });
 
-    it('should not depend on `this`', function () {
+    it('should be used as a predicate', 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'});
+  describe('update(instance, spec)', function () {
 
     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');
+      var Dictionary = t.dict(t.String, t.String);
+      var instance = Dictionary({a: 'a', b: 'b'});
+      var newInstance = Dictionary.update(instance, {b: {$set: 'c'}});
+      assert.ok(Dictionary.is(newInstance));
+      assert.equal(newInstance !== instance, true);
+      assert.deepEqual(newInstance, {a: 'a', b: 'c'});
     });
 
   });
diff --git a/test/es6.js b/test/es6.js
index ef037f8..1e08e3d 100644
--- a/test/es6.js
+++ b/test/es6.js
@@ -69,7 +69,7 @@ describe('ES6 classes', function () {
     assert.deepEqual(T.is(c), true);
     throwsWithMessage(function () {
       T(1);
-    }, '[tcomb] Invalid value 1 supplied to String | Class');
+    }, '[tcomb] Invalid value 1 supplied to String | Class (no constructor found)');
   });
 
   it('should be handled by func', function () {
diff --git a/test/func.js b/test/func.js
index d413972..681d314 100644
--- a/test/func.js
+++ b/test/func.js
@@ -1,18 +1,18 @@
 /* globals describe, it */
 var assert = require('assert');
 var t = require('../index');
-var throwsWithMessage = require('./util').throwsWithMessage;
+var util = require('./util');
 
 describe('t.func(domain, codomain, [name])', function () {
 
-  it('should handle a no types', function () {
+  it('should handle an empty domain', 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 () {
+  it('should handle a domain with 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);
@@ -34,11 +34,11 @@ describe('t.func(domain, codomain, [name])', function () {
       });
       assert.deepEqual(sum(1, 2), 3);
 
-      throwsWithMessage(function () {
+      util.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 () {
+      util.throwsWithMessage(function () {
         sum('a', 2);
       }, '[tcomb] Invalid value "a" supplied to [Number, Number]/0: Number');
 
@@ -51,7 +51,7 @@ describe('t.func(domain, codomain, [name])', function () {
         return 'a';
       });
 
-      throwsWithMessage(function () {
+      util.throwsWithMessage(function () {
         sum(1, 2);
       }, '[tcomb] Invalid value "a" supplied to Number');
 
@@ -92,7 +92,7 @@ describe('t.func(domain, codomain, [name])', 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) {
+      var sum = Type(function (a, b, c) {
         return a + b + c;
       }, true);
       assert.deepEqual(sum(1, 2, 3), 6);
@@ -115,11 +115,11 @@ describe('t.func(domain, codomain, [name])', function () {
         return a + b;
       }, true);
 
-      throwsWithMessage(function () {
+      util.throwsWithMessage(function () {
         sum('a');
       }, '[tcomb] Invalid value "a" supplied to [Number]/0: Number');
 
-      throwsWithMessage(function () {
+      util.throwsWithMessage(function () {
         var sum1 = sum(1);
         sum1('a');
       }, '[tcomb] Invalid value "a" supplied to [Number]/0: Number');
@@ -136,7 +136,7 @@ describe('t.func(domain, codomain, [name])', function () {
         return a + b + c;
       });
       assert.deepEqual(sum(1, 2, 3), 6);
-      throwsWithMessage(function () {
+      util.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/intersection.js b/test/intersection.js
index e2e0a12..0dadf04 100644
--- a/test/intersection.js
+++ b/test/intersection.js
@@ -1,7 +1,7 @@
 /* globals describe, it */
 var assert = require('assert');
 var t = require('../index');
-var throwsWithMessage = require('./util').throwsWithMessage;
+var util = require('./util');
 
 describe('t.intersection(types, [name])', function () {
 
@@ -13,19 +13,19 @@ describe('t.intersection(types, [name])', function () {
 
     it('should throw if used with wrong arguments', function () {
 
-      throwsWithMessage(function () {
+      util.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 () {
+      util.throwsWithMessage(function () {
         t.intersection([]);
       }, '[tcomb] Invalid argument types [] supplied to intersection(types, [name]) combinator (expected an array of at least 2 types)');
 
-      throwsWithMessage(function () {
+      util.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 () {
+      util.throwsWithMessage(function () {
         t.intersection([Min, Max], 1);
       }, '[tcomb] Invalid argument name 1 supplied to intersection(types, [name]) combinator (expected a string)');
 
@@ -35,18 +35,24 @@ describe('t.intersection(types, [name])', function () {
 
   describe('constructor', function () {
 
-    it('should throw if used with wrong arguments', function () {
-      throwsWithMessage(function () {
+    it('should throw with a contextual error message if used with wrong arguments', function () {
+
+      util.throwsWithMessage(function () {
         MinMax('a')
       }, '[tcomb] Invalid value "a" supplied to MinMax');
+
+      util.throwsWithMessage(function () {
+        MinMax('a', ['root'])
+      }, '[tcomb] Invalid value "a" supplied to root');
+
     });
 
   });
 
-  describe('#is(x)', function () {
+  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('123'), true);
       assert.strictEqual(MinMax.is('12'), false);
       assert.strictEqual(MinMax.is('12345'), false);
     });
diff --git a/test/list.js b/test/list.js
index d1e6ac3..922d0f3 100644
--- a/test/list.js
+++ b/test/list.js
@@ -31,16 +31,7 @@ describe('t.list(type, [name])', function () {
     var MyElement = t.struct({}, 'MyElement');
     var MyList = t.list(MyElement, 'MyList');
     var ListOfNumbers = t.list(t.Number, 'ListOfNumbers');
-
-    it('should hydrate the elements of the list', function () {
-      var instance = MyList([{}]);
-      assert.equal(MyElement.is(instance[0]), true);
-    });
-
-    it('should hydrate the elements of the list in production', util.production(function () {
-      var instance = MyList([{}]);
-      assert.equal(MyElement.is(instance[0]), true);
-    }));
+    var Path = t.list(Point, 'Path');
 
     it('should throw with a contextual error message if used with wrong arguments', function () {
 
@@ -58,28 +49,50 @@ describe('t.list(type, [name])', function () {
 
     });
 
+    it('should hydrate the elements of the list', function () {
+      var instance = MyList([{}]);
+      assert.equal(MyElement.is(instance[0]), true);
+    });
+
+    it('should hydrate the elements of the list in production', util.production(function () {
+      var instance = MyList([{}]);
+      assert.equal(MyElement.is(instance[0]), true);
+    }));
+
     it('should be idempotent', function () {
-      var p0 = [1, 2];
-      var p1 = ListOfNumbers(p0);
-      var p2 = ListOfNumbers(p1);
-      assert.equal(p0 === p1, true);
-      assert.equal(p1 === p2, true);
+      var numbers0 = [1, 2];
+      var numbers1 = ListOfNumbers(numbers0);
+      var numbers2 = ListOfNumbers(numbers1);
+      assert.equal(numbers0 === numbers1, true);
+      assert.equal(numbers1 === numbers2, true);
+
+      var path0 = [{x: 0, y: 0}, {x: 1, y: 1}];
+      var path1 = Path(path0);
+      var path2 = Path(path1);
+      assert.equal(path0 === path1, false);
+      assert.equal(path1 === path2, true);
     });
 
     it('should be idempotent in production', util.production(function () {
-      var p0 = [1, 2];
-      var p1 = ListOfNumbers(p0);
-      var p2 = ListOfNumbers(p1);
-      assert.equal(p0 === p1, true);
-      assert.equal(p1 === p2, true);
+      var numbers0 = [1, 2];
+      var numbers1 = ListOfNumbers(numbers0);
+      var numbers2 = ListOfNumbers(numbers1);
+      assert.equal(numbers0 === numbers1, true);
+      assert.equal(numbers1 === numbers2, true);
+
+      var path0 = [{x: 0, y: 0}, {x: 1, y: 1}];
+      var path1 = Path(path0);
+      var path2 = Path(path1);
+      assert.equal(path0 === path1, false);
+      assert.equal(path1 === path2, true);
     }));
 
-    it('should freeze the value', function () {
+    it('should freeze the instance', function () {
       var instance = ListOfNumbers([1, 2]);
       assert.equal(Object.isFrozen(instance), true);
     });
 
-    it('should not freeze the value in production', util.production(function () {
+    it('should not freeze the instance in production', util.production(function () {
       var instance = ListOfNumbers([1, 2]);
       assert.equal(Object.isFrozen(instance), false);
     }));
diff --git a/test/maybe.js b/test/maybe.js
index e433134..f332c43 100644
--- a/test/maybe.js
+++ b/test/maybe.js
@@ -1,7 +1,7 @@
 /* globals describe, it */
 var assert = require('assert');
 var t = require('../index');
-var throwsWithMessage = require('./util').throwsWithMessage;
+var util = require('./util');
 
 var Point = t.struct({
   x: t.Number,
@@ -14,11 +14,11 @@ describe('t.maybe(type, [name])', function () {
 
     it('should throw if used with wrong arguments', function () {
 
-      throwsWithMessage(function () {
+      util.throwsWithMessage(function () {
         t.maybe();
       }, '[tcomb] Invalid argument type undefined supplied to maybe(type, [name]) combinator (expected a type)');
 
-      throwsWithMessage(function () {
+      util.throwsWithMessage(function () {
         t.maybe(Point, 1);
       }, '[tcomb] Invalid argument name 1 supplied to maybe(type, [name]) combinator (expected a string)');
 
@@ -29,6 +29,11 @@ describe('t.maybe(type, [name])', function () {
       assert.ok(t.maybe(MaybeStr) === MaybeStr);
     });
 
+    it('should be idempotent in production', util.production(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);
     });
@@ -42,37 +47,53 @@ describe('t.maybe(type, [name])', function () {
   describe('constructor', function () {
 
     it('should throw if used with new', function () {
-      throwsWithMessage(function () {
+      util.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 () {
+    it('should hydrate the elements of the maybe', 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 hydrate the elements of the maybe in production', util.production(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 p0 = {x: 0, y: 0};
+      var p1 = T();
       var p2 = T(p1);
-      assert.deepEqual(Object.isFrozen(p1), true);
-      assert.deepEqual(Object.isFrozen(p2), true);
-      assert.deepEqual(p2 === p1, true);
+      assert.equal(p0 === p1, false);
+      assert.equal(p1 === p2, true);
     });
 
+    it('should be idempotent in production', util.production(function () {
+      var T = t.maybe(Point);
+      var p0 = {x: 0, y: 0};
+      var p1 = T();
+      var p2 = T(p1);
+      assert.equal(p0 === p1, false);
+      assert.equal(p1 === p2, true);
+    }));
+
   });
 
-  describe('#is(x)', function () {
+  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));
+      assert.strictEqual(Radio.is('a'), true);
+      assert.strictEqual(Radio.is(null), true);
+      assert.strictEqual(Radio.is(undefined), true);
     });
 
   });
diff --git a/test/refinement.js b/test/refinement.js
new file mode 100644
index 0000000..7a2f39d
--- /dev/null
+++ b/test/refinement.js
@@ -0,0 +1,115 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var util = require('./util');
+
+var Point = t.struct({
+  x: t.Number,
+  y: t.Number
+});
+
+describe('t.refinement(type, predicate, [name])', function () {
+
+  var True = function () { return true; };
+
+  describe('combinator', function () {
+
+    it('should throw if used with wrong arguments', function () {
+
+      util.throwsWithMessage(function () {
+        t.refinement();
+      }, '[tcomb] Invalid argument type undefined supplied to refinement(type, predicate, [name]) combinator (expected a type)');
+
+      util.throwsWithMessage(function () {
+        t.refinement(Point, null);
+      }, '[tcomb] Invalid argument predicate supplied to refinement(type, predicate, [name]) combinator (expected a function)');
+
+      util.throwsWithMessage(function () {
+        t.refinement(Point, True, 1);
+      }, '[tcomb] Invalid argument name 1 supplied to refinement(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 () {
+      util.throwsWithMessage(function () {
+        var T = t.refinement(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 throw with a contextual error message if used with wrong arguments', function () {
+      var predicate = function (p) { return p.x > 0; };
+      var T = t.refinement(Point, predicate, 'T');
+      util.throwsWithMessage(function () {
+        T({x: 0, y: 0});
+      }, '[tcomb] Invalid value {\n  "x": 0,\n  "y": 0\n} supplied to T');
+    });
+
+    it('should hydrate the elements of the refinement', function () {
+      var T = t.refinement(Point, function () { return true; });
+      var p = T({x: 0, y: 0});
+      assert.ok(Point.is(p));
+    });
+
+    it('should hydrate the elements of the refinement in production', util.production(function () {
+      var T = t.refinement(Point, function () { return true; });
+      var p = T({x: 0, y: 0});
+      assert.ok(Point.is(p));
+    }));
+
+    it('should be idempotent', function () {
+      var Supertype = t.dict(t.String, t.Number);
+      var Refinement = t.refinement(Supertype, function () { return true; });
+      var t0 = {};
+      var t1 = Refinement(t0);
+      var t2 = Refinement(t1);
+      assert.equal(t0 === t1, true);
+      assert.equal(t1 === t2, true);
+    });
+
+    it('should be idempotent in production', util.production(function () {
+      var Supertype = t.dict(t.String, t.Number);
+      var Refinement = t.refinement(Supertype, function () { return true; });
+      var t0 = {};
+      var t1 = Refinement(t0);
+      var t2 = Refinement(t1);
+      assert.equal(t0 === t1, true);
+      assert.equal(t1 === t2, true);
+    }));
+
+  });
+
+  describe('is(x)', function () {
+
+    var Positive = t.refinement(t.Number, function (n) {
+      return n >= 0;
+    });
+
+    it('should return true when x is a refinement', function () {
+      assert.ok(Positive.is(1));
+    });
+
+    it('should return false when x is not a refinement', function () {
+      assert.strictEqual(Positive.is(-1), false);
+    });
+
+  });
+
+  describe('update(instance, spec)', function () {
+
+    var Type = t.refinement(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/subtype.js b/test/subtype.js
deleted file mode 100644
index d23d534..0000000
--- a/test/subtype.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/* 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/tuple.js b/test/tuple.js
index 6d775f8..c06bee6 100644
--- a/test/tuple.js
+++ b/test/tuple.js
@@ -1,7 +1,7 @@
 /* globals describe, it */
 var assert = require('assert');
 var t = require('../index');
-var throwsWithMessage = require('./util').throwsWithMessage;
+var util = require('./util');
 
 var Point = t.struct({
   x: t.Number,
@@ -16,15 +16,15 @@ describe('t.tuple(types, [name])', function () {
 
     it('should throw if used with wrong arguments', function () {
 
-      throwsWithMessage(function () {
+      util.throwsWithMessage(function () {
         t.tuple();
       }, '[tcomb] Invalid argument types undefined supplied to tuple(types, [name]) combinator (expected an array of types)');
 
-      throwsWithMessage(function () {
+      util.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 () {
+      util.throwsWithMessage(function () {
         t.tuple([Point, Point], 1);
       }, '[tcomb] Invalid argument name 1 supplied to tuple(types, [name]) combinator (expected a string)');
 
@@ -37,36 +37,49 @@ describe('t.tuple(types, [name])', 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 throw with a contextual error message if used with wrong arguments', function () {
+
+      var T = t.tuple([t.Number, t.Number], 'T');
 
-    it('should accept only valid values', function () {
+      util.throwsWithMessage(function () {
+        T();
+      }, '[tcomb] Invalid value undefined supplied to T (expected an array of length 2)');
 
-      throwsWithMessage(function () {
-        T(1);
-      }, '[tcomb] Invalid value 1 supplied to T (expected an array of length 2)');
+      util.throwsWithMessage(function () {
+        T(['a']);
+      }, '[tcomb] Invalid value [\n  "a"\n] 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 hydrate the elements of the list', function () {
+      var instance = T([{}, {}]);
+      assert.equal(S.is(instance[0]), true);
     });
 
+    it('should hydrate the elements of the list in production', util.production(function () {
+      var instance = T([{}, {}]);
+      assert.equal(S.is(instance[0]), true);
+    }));
+
     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);
+      var t0 = [{}, {}];
+      var t1 = T(t0);
+      var t2 = T(t1);
+      assert.equal(t0 === t1, false);
+      assert.equal(t1 === t2, true);
     });
 
+    it('should be idempotent in production', util.production(function () {
+      var t0 = [{}, {}];
+      var t1 = T(t0);
+      var t2 = T(t1);
+      assert.equal(t0 === t1, false);
+      assert.equal(t1 === t2, true);
+    }));
+
   });
 
-  describe('#is(x)', function () {
+  describe('is(x)', function () {
 
     it('should return true when x is an instance of the tuple', function () {
       assert.ok(Area.is([1, 2]));
@@ -84,16 +97,15 @@ describe('t.tuple(types, [name])', function () {
 
   });
 
-  describe('#update()', function () {
-
-    var Type = t.tuple([t.String, t.Number]);
-    var instance = Type(['a', 1]);
+  describe('update(instance, spec)', function () {
 
     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');
+      var Tuple = t.tuple([t.String, t.Number]);
+      var instance = Tuple(['a', 1]);
+      var newInstance = Tuple.update(instance, {0: { $set: 'b' }});
+      assert.equal(Tuple.is(newInstance), true);
+      assert.equal(newInstance !== instance, true);
+      assert.equal(newInstance[0], 'b');
     });
 
   });
diff --git a/test/union.js b/test/union.js
index 8e735da..246abc8 100644
--- a/test/union.js
+++ b/test/union.js
@@ -1,7 +1,7 @@
 /* globals describe, it */
 var assert = require('assert');
 var t = require('../index');
-var throwsWithMessage = require('./util').throwsWithMessage;
+var util = require('./util');
 
 var Point = t.struct({
   x: t.Number,
@@ -21,7 +21,7 @@ var Rectangle = t.struct({
 var Shape = t.union([Circle, Rectangle], 'Shape');
 
 Shape.dispatch = function (values) {
-  assert(t.Object.is(values));
+  values = t.Object(values);
   return values.hasOwnProperty('center') ?
     Circle :
     Rectangle;
@@ -33,19 +33,19 @@ describe('t.union(types, [name])', function () {
 
     it('should throw if used with wrong arguments', function () {
 
-      throwsWithMessage(function () {
+      util.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 () {
+      util.throwsWithMessage(function () {
         t.union([]);
       }, '[tcomb] Invalid argument types [] supplied to union(types, [name]) combinator (expected an array of at least 2 types)');
 
-      throwsWithMessage(function () {
+      util.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 () {
+      util.throwsWithMessage(function () {
         t.union([Circle, Point], 1);
       }, '[tcomb] Invalid argument name 1 supplied to union(types, [name]) combinator (expected a string)');
 
@@ -55,80 +55,113 @@ describe('t.union(types, [name])', function () {
 
   describe('constructor', function () {
 
-    it('should build instances when dispatch() is implemented', function () {
+    it('should hydrate the input', function () {
       var circle = Shape({center: {x: 0, y: 0}, radius: 10});
-      assert.ok(Circle.is(circle));
+      assert.equal(Circle.is(circle), true);
     });
 
-    it('should throw if used with new and union types are not instantiables with new', function () {
-      throwsWithMessage(function () {
+    it('should hydrate the input in production', util.production(function () {
+      var circle = Shape({center: {x: 0, y: 0}, radius: 10});
+      assert.equal(Circle.is(circle), true);
+    }));
+
+    it('should freeze the instance', function () {
+      var circle = Shape({center: {x: 0, y: 0}, radius: 10});
+      assert.equal(Object.isFrozen(circle), true);
+    });
+
+    it('should not freeze the instance in production', util.production(function () {
+      var circle = Shape({center: {x: 0, y: 0}, radius: 10});
+      assert.equal(Object.isFrozen(circle), false);
+    }));
+
+    it('should throw if used with new', function () {
+
+      assert.doesNotThrow(function () {
+        Shape({center: {x: 0, y: 0}, radius: 10});
+      });
+
+      util.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 show the offended union type in error messages', function () {
-      throwsWithMessage(function () {
+      util.throwsWithMessage(function () {
         Shape({center: {x: 0, y: 0}});
       }, '[tcomb] Invalid value undefined supplied to Shape(Circle)/radius: Number');
     });
 
-    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);
+      var s0 = {center: {x: 0, y: 0}, radius: 10};
+      var s1 = Shape(s0);
+      var s2 = Shape(s1);
+      assert.equal(s1 === s0, false);
+      assert.equal(s2 === s1, true);
     });
 
+    it('should be idempotent i production', util.production(function () {
+      var s0 = {center: {x: 0, y: 0}, radius: 10};
+      var s1 = Shape(s0);
+      var s2 = Shape(s1);
+      assert.equal(s1 === s0, false);
+      assert.equal(s2 === s1, true);
+    }));
+
   });
 
-  describe('#is(x)', function () {
+  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));
+      var p = new Circle({center: {x: 0, y: 0}, radius: 10});
+      assert.equal(Shape.is(p), true);
+      assert.equal(Shape.is(1), false);
     });
 
   });
 
-  describe('#dispatch(x)', function () {
+  describe('dispatch(x)', function () {
 
     it('should have a default implementation', function () {
-      var T = t.union([t.String, t.Number], 'T');
-      assert.deepEqual(T(1), 1);
+      var U = t.union([t.String, t.Number], 'T');
+      assert.equal(U(1), 1);
     });
 
     it('should handle union of unions', function () {
-      var T1 = t.union([t.String, t.Number], 'T1');
-      var T2 = t.union([t.Boolean, t.Object], 'T2');
-      var T = t.union([T1, T2, t.Array], 'T');
-      assert.strictEqual(T.dispatch(1), t.Number);
-      assert.strictEqual(T.dispatch({foo: "bar"}), t.Object);
-      assert.strictEqual(T.dispatch([]), t.Array);
+      var U1 = t.union([t.String, t.Number], 'U1');
+      var U2 = t.union([t.Boolean, t.Object], 'U2');
+      var U = t.union([U1, U2, t.Array], 'U');
+      assert.equal(U.dispatch(1), t.Number);
+      assert.equal(U.dispatch({foo: "bar"}), t.Object);
+      assert.equal(U.dispatch([]), t.Array);
     });
 
-    it('should throw if 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 throw if does not return a valid type', function () {
+
+      util.throwsWithMessage(function () {
+        var U = t.union([t.String, t.Number], 'T');
+        U(true);
+      }, '[tcomb] Invalid value true supplied to T (no constructor found)');
+
+      util.throwsWithMessage(function () {
+        var U = t.union([t.String, t.Number], 'T');
+        U.dispatch = function () {
+          return t.Boolean;
+        };
+        U(true);
+      }, '[tcomb] Invalid constructor Boolean returned by T.dispatch(x) function');
+
     });
 
   });
 
-  describe('#update(instance, spec)', function () {
+  describe('update(instance, spec)', function () {
 
     it('should update the right instance', function () {
       var circle = Shape.update({ center: { x: 0, y: 0 }, radius: 10 }, { radius: { $set: 15 } });
-      assert.strictEqual(Circle.is(circle), true);
+      assert.equal(Circle.is(circle), true);
     });
 
   });
diff --git a/test/util.js b/test/util.js
index ef67a9e..693a5aa 100644
--- a/test/util.js
+++ b/test/util.js
@@ -11,8 +11,14 @@ function throwsWithMessage(f, message) {
 function production(f) {
   return function () {
     process.env.NODE_ENV = 'production';
-    f();
-    process.env.NODE_ENV = 'development';
+    try {
+      f();
+    } catch (e) {
+      assert.fail(e.message);
+    }
+    finally {
+      process.env.NODE_ENV = 'development';
+    }
   };
 }