Repository: gcanti/io-ts (ts)

Issue:
intersections don't play well with exact
## 🐛 Bug report

### Reproducible example

```ts
const C = t.intersection([t.exact(t.type({})), t.exact(t.partial({ a: t.number }))])
assert.deepStrictEqual(C.decode({ a: 1 }), right({ a: 1 }))
assert.deepStrictEqual(C.decode({ a: 1, b: 1 }), right({ a: 1 }))
```

### Additional context

RE: https://github.com/gcanti/io-ts/pull/390

### Your environment

| Software   | Version(s) |
| ---------- | ---------- |
| io-ts      |  2.0.1          |
| fp-ts      |  2.0.0          |
| TypeScript |  3.7.2          |


Pull request description:
fix #397

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 905243e..927df20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,11 @@
 **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.
 
+# 2.0.2
+
+- **Bug Fix**
+  - fix #397 (@gcanti)
+
 # 2.0.1
 
 - **Bug Fix**
diff --git a/package.json b/package.json
index 60aaec0..f5bcafa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "io-ts",
-  "version": "2.0.1",
+  "version": "2.0.2",
   "description": "TypeScript compatible runtime type system for IO validation",
   "files": [
     "lib",
diff --git a/src/index.ts b/src/index.ts
index 93b099f..1f71108 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1191,19 +1191,26 @@ export interface IntersectionC<CS extends [Mixed, Mixed, ...Array<Mixed>]>
   > {}
 
 const mergeAll = (base: any, us: Array<any>): any => {
-  let r: any = base
-  for (let i = 0; i < us.length; i++) {
-    const u = us[i]
+  let equal = true
+  let primitive = true
+  for (const u of us) {
     if (u !== base) {
-      // `u` contains a prismatic value or is the result of a stripping combinator
-      if (r === base) {
-        r = Object.assign({}, u)
-        continue
-      }
-      for (const k in u) {
-        if (u[k] !== base[k] || !r.hasOwnProperty(k)) {
-          r[k] = u[k]
-        }
+      equal = false
+    }
+    if (isObject(u)) {
+      primitive = false
+    }
+  }
+  if (equal) {
+    return base
+  } else if (primitive) {
+    return us[us.length - 1]
+  }
+  let r: any = {}
+  for (const u of us) {
+    for (const k in u) {
+      if (u[k] !== base[k] || !r.hasOwnProperty(k)) {
+        r[k] = u[k]
       }
     }
   }


Test patch:
diff --git a/test/intersection.ts b/test/intersection.ts
index 9246aa6..014c45e 100644
--- a/test/intersection.ts
+++ b/test/intersection.ts
@@ -87,15 +87,32 @@ describe('intersection', () => {
       assertStrictEqual(T.decode(value), value)
     })
 
-    it('should play well with stripping combinators', () => {
-      const A = t.exact(t.type({ a: t.string }))
-      const B = t.exact(t.type({ b: t.number }))
-      const T = t.intersection([A, B])
-      assertSuccess(T.decode({ a: 'a', b: 1 }))
-      assertSuccess(T.decode({ a: 'a', b: 1, c: true }), { a: 'a', b: 1 })
-      assertFailure(T, { a: 'a' }, [
+    it('should play well with exact', () => {
+      const T1 = t.intersection([t.exact(t.type({ a: t.string })), t.exact(t.type({ b: t.number }))])
+      assertSuccess(T1.decode({ a: 'a', b: 1 }), { a: 'a', b: 1 })
+      assertSuccess(T1.decode({ a: 'a', b: 1, c: true }), { a: 'a', b: 1 })
+      assertFailure(T1, { a: 'a' }, [
         'Invalid value undefined supplied to : ({| a: string |} & {| b: number |})/1: {| b: number |}/b: number'
       ])
+
+      const T2 = t.intersection([t.exact(t.type({})), t.partial({ a: t.number })])
+      assertSuccess(T2.decode({}), {})
+      assertSuccess(T2.decode({ a: 1 }), { a: 1 })
+      assertSuccess(T2.decode({ a: undefined }), { a: undefined })
+      assertSuccess(T2.decode({ a: 1, b: true }), { a: 1, b: true } as any)
+
+      // #397
+      const T3 = t.intersection([t.exact(t.type({})), t.exact(t.partial({ a: t.number }))])
+      assertSuccess(T3.decode({}), {})
+      assertSuccess(T3.decode({ a: 1 }), { a: 1 })
+      assertSuccess(T3.decode({ a: undefined }), { a: undefined })
+      assertSuccess(T3.decode({ a: 1, b: true }), { a: 1 })
+
+      const T4 = t.intersection([t.type({ b: t.string }), t.exact(t.partial({ a: t.number }))])
+      assertSuccess(T4.decode({ b: 'b' }), { b: 'b' })
+      assertSuccess(T4.decode({ b: 'b', a: 1 }), { b: 'b', a: 1 })
+      assertSuccess(T4.decode({ b: 'b', a: undefined }), { b: 'b', a: undefined })
+      assertSuccess(T4.decode({ b: 'b', a: 1, c: 2 }), { b: 'b', a: 1, c: 2 } as any)
     })
   })
 
@@ -149,8 +166,23 @@ describe('intersection', () => {
     const T = t.intersection([t.string] as any)
     assert.strictEqual(T.is('a'), true)
     assert.strictEqual(T.is(1), false)
-    assertSuccess(T.decode('a'))
+    assertSuccess(T.decode('a'), 'a')
     assertFailure(T, 1, ['Invalid value 1 supplied to : (string)/0: string'])
     assert.strictEqual(T.encode('a'), 'a')
   })
+
+  it('should handle primitives', () => {
+    const T1 = t.intersection([t.string, t.string])
+    assert.strictEqual(T1.is('a'), true)
+    assert.strictEqual(T1.is(1), false)
+    assertSuccess(T1.decode('a'), 'a')
+    assertFailure(T1, 1, [
+      'Invalid value 1 supplied to : (string & string)/0: string',
+      'Invalid value 1 supplied to : (string & string)/1: string'
+    ])
+    assert.strictEqual(T1.encode('a'), 'a')
+    const T2 = t.intersection([NumberFromString, NumberFromString])
+    assertSuccess(T2.decode('1'), 1)
+    assert.strictEqual(T2.encode(1), '1')
+  })
 })