Repository: gcanti/tcomb (js)

Issue:
Proposal for top-level extends
Forgive me if this has already been asked; I checked but didn't find a similar issue.

Why is `extend` a method of Struct constructors?  For example:

```javascript
const A = t.struct({
    a: t.String
}, 'A');

const B = t.struct({
    b: t.String
}, 'B');

const C = t.struct({
    c: t.Number
}, 'C');

const D = C.extend([A, B], 'D');

const d = new D({
    a: 'a',
    b: 'b',
    c: 4
});

console.log(d instanceof D); // true
console.log(d instanceof C); // false

```

Why should D = C.extend, when the inheritance chain isn't preserved?  The same is true for A and B.

I understand why we **wouldn't** want it to work that way, but given that, what if we had:

```javascript
const D = t.extend([A,B,C], 'D');
```
(feel free to replace `extend` with `mix` or even `t.struct.combine` if the other two are too closely named to other methods)

That way, when I want an intersection of structs, so to speak, I don't have to pick a "base" struct to extend from?  Maybe it's a bit of an arbitrary or trivial pain point, but I think there's a bit of a mental disconnect between what's actually happening, and the implication of extend belonging to a Struct constructor.

I'd be happy to take a stab at a putting together a pull request if you'd like.

Pull request:
fix #164

Patch:
diff --git a/docs/API.md b/docs/API.md
index 76d2072..e32bffe 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -297,6 +297,12 @@ Cube.prototype.getVolume = function () {
 const Wrong = Point.extend({ x: t.Number }); // => throws '[tcomb] Invalid call to mixin(target, source, [overwrite]): cannot overwrite property "x" of target object'
 ```
 
+Alternatively you can use the `t.struct.extend(mixins: Array<Mixin>, name?: string) => TcombType` function:
+
+```js
+const Point3D = t.struct.extend([Point, { z: t.Number }], 'Point3D');
+```
+
 # The `tuple` combinator
 
 **Signature**
diff --git a/lib/struct.js b/lib/struct.js
index 660dd02..3d832e1 100644
--- a/lib/struct.js
+++ b/lib/struct.js
@@ -2,6 +2,7 @@ var assert = require('./assert');
 var isTypeName = require('./isTypeName');
 var String = require('./String');
 var Function = require('./Function');
+var isArray = require('./isArray');
 var isObject = require('./isObject');
 var create = require('./create');
 var mixin = require('./mixin');
@@ -15,6 +16,28 @@ function getDefaultName(props) {
   }).join(', ') + '}';
 }
 
+function extend(mixins, name) {
+  if (process.env.NODE_ENV !== 'production') {
+    assert(isArray(mixins) && mixins.every(function (x) {
+      return isObject(x) || isStruct(x);
+    }), function () { return 'Invalid argument mixins supplied to extend(mixins, name), expected an array of objects or types'; });
+  }
+  var props = {};
+  var prototype = {};
+  mixins.forEach(function (struct) {
+    if (isObject(struct)) {
+      mixin(props, struct);
+    }
+    else {
+      mixin(props, struct.meta.props);
+      mixin(prototype, struct.prototype);
+    }
+  });
+  var ret = struct(props, name);
+  mixin(ret.prototype, prototype);
+  return ret;
+}
+
 function struct(props, name) {
 
   if (process.env.NODE_ENV !== 'production') {
@@ -71,27 +94,12 @@ function struct(props, name) {
   };
 
   Struct.extend = function (structs, name) {
-    var props = {};
-    var prototype = {};
-    [Struct].concat(structs).forEach(function (struct, i) {
-      if (isObject(struct)) {
-        mixin(props, struct);
-      }
-      else {
-        if (process.env.NODE_ENV !== 'production') {
-          assert(isStruct(struct), function () { return 'Invalid argument structs[' + i + '] ' + assert.stringify(struct) + ' supplied to ' + displayName + '.extend(structs, name)'; });
-        }
-        mixin(props, struct.meta.props);
-        mixin(prototype, struct.prototype);
-      }
-    });
-    var ret = struct(props, name);
-    mixin(ret.prototype, prototype);
-    return ret;
+    return extend([Struct].concat(structs), name);
   };
 
   return Struct;
 }
 
 struct.getDefaultName = getDefaultName;
+struct.extend = extend;
 module.exports = struct;


Test patch:
diff --git a/test/struct.js b/test/struct.js
index 655beb8..288dd61 100644
--- a/test/struct.js
+++ b/test/struct.js
@@ -30,6 +30,20 @@ describe('t.struct(props, [name])', function () {
 
   });
 
+  describe('struct.extend', function () {
+    it('should handle an array of mixins', function () {
+      var Point = t.struct({
+        x: t.Number,
+        y: t.Number
+      }, 'Point');
+      var Point3D = t.struct.extend([Point, {z: t.Number}], 'Point3D');
+      assert.deepEqual(Point3D.meta.name, 'Point3D', 'name');
+      assert.deepEqual(Point3D.meta.props.x, t.Number, 'x');
+      assert.deepEqual(Point3D.meta.props.y, t.Number, 'y');
+      assert.deepEqual(Point3D.meta.props.z, t.Number, 'z');
+    });
+  });
+
   describe('constructor', function () {
 
     it('should be idempotent', function () {