Repository: gcanti/tcomb (js)

Issue:
Allow overriding base defaultProps in extended structs
### Version

3.2.14

### Expected behaviour

When extending an existing struct with some defaultProps set, the extended struct should be able to override base type's defaultProps values.

### Actual behaviour

A `mixin` call within `extendStruct` will throw an exception.

### Steps to reproduce

```
var BasePanelConfig = t.struct(
    { collapsed: t.Boolean, helpKey: t.maybe(t.String) },
    { defaultProps: { collapsed: false } }
);
var MyPanelConfig = BasePanelConfig.extend(
    { myThing: t.maybe(t.String) },
    { defaultProps: { collapsed: true, helpKey: "my-help-key" } }
)
```

### Stack trace and console log

```
> process.version  // node ver
'v6.6.0
> var t = require("tcomb")
> BasePanelConfig = t.struct({ collapsed: t.Boolean, helpKey: t.maybe(t.String) }, { defaultProps: { collapsed: false } } )
{ [Function: Struct]
  meta:
   { kind: 'struct',
     props: { collapsed: [Object], helpKey: [Object] },
     name: undefined,
     identity: false,
     strict: false,
     defaultProps: { collapsed: false } },
  displayName: 'Struct{collapsed: Boolean, helpKey: ?String}',
  is: [Function],
  update: [Function],
  extend: [Function] }
> MyPanelConfig = BasePanelConfig.extend({ myThing: t.maybe(t.String) }, { defaultProps: { collapsed: true, helpKey: "my-help-key" } })
TypeError: [tcomb] Invalid call to mixin(target, source, [overwrite]): cannot overwrite property "collapsed" of target object
    at Function.fail (./node_modules/tcomb/lib/fail.js:2:9)
    at assert (./node_modules/tcomb/lib/assert.js:14:12)
    at mixin (./node_modules/tcomb/lib/mixin.js:11:11)
    at extend (./node_modules/tcomb/lib/extend.js:50:3)
    at extendStruct (./node_modules/tcomb/lib/struct.js:19:10)
    at Function.Struct.extend (./node_modules/tcomb/lib/struct.js:112:12)
    at repl:1:33
    at sigintHandlersWrap (vm.js:22:35)
    at sigintHandlersWrap (vm.js:96:12)
    at ContextifyScript.Script.runInThisContext (vm.js:21:12)
```


Pull request description:
Added support for overwriting `defaultProps` in `t.struct.extend`
Fixes #257.

Patch:
diff --git a/docs/API.md b/docs/API.md
index a7764c6..4a8fbe0 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -337,6 +337,27 @@ Alternatively you can use the `t.struct.extend(mixins: Array<Mixin>, options?: s
 const Point3D = t.struct.extend([Point, { z: t.Number }], 'Point3D');
 ```
 
+**Note**. Repeated defaultProps overwrite previously set values:
+
+```js
+const Base = t.struct(
+  { value: t.String },
+  { defaultProps: { value: "base" }}
+);
+const Extended = Base.extend(
+  {},
+  { defaultProps: { value: "override-extended" }}
+);
+const DeepExtended = t.struct.extend(
+  [Base, Extended, {}],
+  { defaultProps: { value: "override-deep" }}
+);
+
+Base({}).value;          // equals 'base'
+Extended({}).value;      // equals 'override-extended'
+DeepExtended({}).value;  // equals 'override-deep'
+```
+
 **Note**. The implementation uses the top level function `extend(combinator, mixins, options)` defined in `tcomb/lib/extend`
 
 # The `tuple` combinator
diff --git a/lib/extend.js b/lib/extend.js
index eb514aa..63754ac 100644
--- a/lib/extend.js
+++ b/lib/extend.js
@@ -44,10 +44,10 @@ function extend(combinator, mixins, options) {
     pushAll(predicates, decomposition.predicates);
     mixin(props, getProps(unrefinedType));
     mixin(prototype, unrefinedType.prototype);
-    mixin(defaultProps, getDefaultProps(unrefinedType));
+    mixin(defaultProps, getDefaultProps(unrefinedType), true);
   });
   options = combinator.getOptions(options);
-  mixin(options.defaultProps, defaultProps);
+  options.defaultProps = mixin(defaultProps, options.defaultProps, true);
   var result = compose(predicates, combinator(props, options));
   mixin(result.prototype, prototype);
   return result;


Test patch:
diff --git a/test/extend.js b/test/extend.js
index 8ddf8f8..4bad0ea 100644
--- a/test/extend.js
+++ b/test/extend.js
@@ -53,6 +53,57 @@ describe('extend(combinator, mixins, [name])', function () {
     });
   });
 
+  it('should override defaultProps', function () {
+    var A1 = t.struct({ a: t.String }, { defaultProps: { a: 'default-a' } });
+    var B1 = t.struct({ b: t.String }, { defaultProps: { b: 'default-b' } });
+    var C1 = t.struct.extend([A1, B1, { z: t.Number }], { defaultProps: {
+      a: 'override-c',
+      b: 'override-c',
+      z: 1
+    }});
+    assert.deepEqual(A1.meta.defaultProps, {
+      a: 'default-a'
+    });
+    assert.deepEqual(B1.meta.defaultProps, {
+      b: 'default-b'
+    });
+    assert.deepEqual(C1.meta.defaultProps, {
+      a: 'override-c',
+      b: 'override-c',
+      z: 1
+    });
+    assert.deepEqual(C1({}), {
+      a: 'override-c',
+      b: 'override-c',
+      z: 1
+    });
+  });
+
+  it('should override deep defaultProps', function () {
+    var A1 = t.struct({ z: t.String }, { defaultProps: { z: 'default-a' } });
+    var B1 = t.struct.extend([A1], { defaultProps: { z: 'override-b' } });
+    var C1 = t.struct.extend([B1, {}], { defaultProps: {}});
+    var D1 = t.struct.extend([C1, {}], { defaultProps: { z: 'override-d' }});
+    assert.deepEqual(A1.meta.defaultProps, {
+      z: 'default-a'
+    });
+    assert.deepEqual(B1.meta.defaultProps, {
+      z: 'override-b'
+    });
+    assert.deepEqual(C1.meta.defaultProps, {
+      z: 'override-b'
+    });
+    assert.deepEqual(D1.meta.defaultProps, {
+      z: 'override-d'
+    });
+    assert.deepEqual(D1({}), {
+      z: 'override-d'
+    });
+    assert.deepEqual(D1({ z: 'provided-d' }), {
+      z: 'provided-d'
+    });
+  });
+
   describe('struct.extend(mixins, [name])', function () {
 
     it('should handle an array of mixins', function () {