Repository: gcanti/io-ts (ts)

Issue:
Obtain all errors when decoding D.type
## 🚀 Feature request
Hi I am new to this library and I am currently trying out the new experimental interface. I noticed that the decoders only produce the first error instead of all decoding errors.
### Current Behavior
When decoding fails for a field, the the decoder returns immediately with the error of that field. 

### Desired Behavior
I would like the decoder to continue decoding the remaining fields, collecting all decoding errors. I believe this is the behaviour of the stable implementation. Is there a reason for this change and is there a way to retrieve all errors?

Thank you.
### Your environment

<!-- PLEASE FILL THIS OUT -->

| Software   | Version(s) |
| ---------- | ---------- |
| io-ts      |        2.2.0    |
| fp-ts      |      2.5.3      |
| TypeScript |    3.8.3        |


Pull request description:
collect all errors while decoding, closes #449

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0826cd..342a633 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,8 +11,13 @@
 > - [Experimental]
 > - [Deprecation]
 
-**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.
+**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.2.1
+
+- **Experimental**
+  - collect all errors while decoding, closes #449 (@gcanti)
 
 # 2.2.0
 
diff --git a/Decoder.md b/Decoder.md
index 19e5e3f..26851d5 100644
--- a/Decoder.md
+++ b/Decoder.md
@@ -341,5 +341,7 @@ if (isLeft(result)) {
 /*
 required property "name"
 └─ cannot decode undefined, should be string
+required property "age"
+└─ cannot decode undefined, should be number
 */
 ```
diff --git a/docs/modules/Decoder.ts.md b/docs/modules/Decoder.ts.md
index 1dcacbc..2fe8c8b 100644
--- a/docs/modules/Decoder.ts.md
+++ b/docs/modules/Decoder.ts.md
@@ -26,7 +26,6 @@ Added in v2.2.0
 - [boolean](#boolean)
 - [decoder](#decoder)
 - [failure](#failure)
-- [failures](#failures)
 - [fromGuard](#fromguard)
 - [intersection](#intersection)
 - [lazy](#lazy)
@@ -195,19 +194,6 @@ export declare function failure<A = never>(message: string): Either<NonEmptyArra
 
 Added in v2.2.0
 
-# failures
-
-**Signature**
-
-```ts
-export declare function failures<A = never>(
-  message: string,
-  errors: NonEmptyArray<Tree<string>>
-): Either<NonEmptyArray<Tree<string>>, A>
-```
-
-Added in v2.2.0
-
 # fromGuard
 
 **Signature**
diff --git a/package.json b/package.json
index 758c552..c454f0c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "io-ts",
-  "version": "2.2.0",
-  "description": "TypeScript compatible runtime type system for IO validation",
+  "version": "2.2.1",
+  "description": "TypeScript runtime type system for IO decoding/encoding",
   "files": [
     "lib",
     "es6"
@@ -63,16 +63,16 @@
   },
   "tags": [
     "typescript",
-    "validation",
-    "inference",
-    "types",
-    "runtime"
+    "runtime",
+    "decoder",
+    "encoder",
+    "schema"
   ],
   "keywords": [
     "typescript",
-    "validation",
-    "inference",
-    "types",
-    "runtime"
+    "runtime",
+    "decoder",
+    "encoder",
+    "schema"
   ]
 }
diff --git a/perf/Decoder.ts b/perf/Decoder.ts
new file mode 100644
index 0000000..c7b78ea
--- /dev/null
+++ b/perf/Decoder.ts
@@ -0,0 +1,108 @@
+import * as Benchmark from 'benchmark'
+import * as t from '../src/Decoder'
+
+/*
+space-object (good) x 662,359 ops/sec ±0.65% (88 runs sampled)
+space-object (bad) x 379,528 ops/sec ±0.56% (89 runs sampled)
+*/
+
+const suite = new Benchmark.Suite()
+
+const Vector = t.tuple(t.number, t.number, t.number)
+
+const Asteroid = t.type({
+  type: t.literal('asteroid'),
+  location: Vector,
+  mass: t.number
+})
+
+const Planet = t.type({
+  type: t.literal('planet'),
+  location: Vector,
+  mass: t.number,
+  population: t.number,
+  habitable: t.boolean
+})
+
+const Rank = t.literal('captain', 'first mate', 'officer', 'ensign')
+
+const CrewMember = t.type({
+  name: t.string,
+  age: t.number,
+  rank: Rank,
+  home: Planet
+})
+
+const Ship = t.type({
+  type: t.literal('ship'),
+  location: Vector,
+  mass: t.number,
+  name: t.string,
+  crew: t.array(CrewMember)
+})
+
+const T = t.sum('type')({
+  asteroid: Asteroid,
+  planet: Planet,
+  ship: Ship
+})
+
+const good = {
+  type: 'ship',
+  location: [1, 2, 3],
+  mass: 4,
+  name: 'foo',
+  crew: [
+    {
+      name: 'bar',
+      age: 44,
+      rank: 'captain',
+      home: {
+        type: 'planet',
+        location: [5, 6, 7],
+        mass: 8,
+        population: 1000,
+        habitable: true
+      }
+    }
+  ]
+}
+
+const bad = {
+  type: 'ship',
+  location: [1, 2, 'a'],
+  mass: 4,
+  name: 'foo',
+  crew: [
+    {
+      name: 'bar',
+      age: 44,
+      rank: 'captain',
+      home: {
+        type: 'planet',
+        location: [5, 6, 7],
+        mass: 8,
+        population: 'a',
+        habitable: true
+      }
+    }
+  ]
+}
+
+// console.log(T.decode(good))
+// console.log(T.decode(bad))
+
+suite
+  .add('space-object (good)', function () {
+    T.decode(good)
+  })
+  .add('space-object (bad)', function () {
+    T.decode(bad)
+  })
+  .on('cycle', function (event: any) {
+    console.log(String(event.target))
+  })
+  .on('complete', function (this: any) {
+    console.log('Fastest is ' + this.filter('fastest').map('name'))
+  })
+  .run({ async: true })
diff --git a/perf/index.ts b/perf/index.ts
index a220dc2..b66c2cf 100644
--- a/perf/index.ts
+++ b/perf/index.ts
@@ -2,8 +2,8 @@ import * as Benchmark from 'benchmark'
 import * as t from '../src'
 
 /*
-space-object (good) x 435,472 ops/sec ±0.61% (83 runs sampled)
-space-object (bad) x 392,528 ops/sec ±2.39% (87 runs sampled)
+space-object (good) x 476,424 ops/sec ±0.45% (92 runs sampled)
+space-object (bad) x 434,563 ops/sec ±0.58% (87 runs sampled)
 */
 
 const suite = new Benchmark.Suite()
@@ -71,7 +71,7 @@ const good = {
 
 const bad = {
   type: 'ship',
-  location: [1, 2, 3],
+  location: [1, 2, 'a'],
   mass: 4,
   name: 'foo',
   crew: [
@@ -94,16 +94,16 @@ const bad = {
 // console.log(T.decode(bad))
 
 suite
-  .add('space-object (good)', function() {
+  .add('space-object (good)', function () {
     T.decode(good)
   })
-  .add('space-object (bad)', function() {
+  .add('space-object (bad)', function () {
     T.decode(bad)
   })
-  .on('cycle', function(event: any) {
+  .on('cycle', function (event: any) {
     console.log(String(event.target))
   })
-  .on('complete', function(this: any) {
+  .on('complete', function (this: any) {
     console.log('Fastest is ' + this.filter('fastest').map('name'))
   })
   .run({ async: true })
diff --git a/src/Decoder.ts b/src/Decoder.ts
index 99ef84c..524df45 100644
--- a/src/Decoder.ts
+++ b/src/Decoder.ts
@@ -56,14 +56,8 @@ export function failure<A = never>(message: string): Either<NonEmptyArray<Tree<s
   return left([tree(message)])
 }
 
-/**
- * @since 2.2.0
- */
-export function failures<A = never>(
-  message: string,
-  errors: NonEmptyArray<Tree<string>>
-): Either<NonEmptyArray<Tree<string>>, A> {
-  return left([tree(message, errors)])
+function isNotEmpty<A>(as: Array<A>): as is NonEmptyArray<A> {
+  return as.length > 0
 }
 
 /**
@@ -197,16 +191,17 @@ export function type<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decoder<
         return e
       } else {
         const r = e.right
-        let a: Partial<A> = {}
+        const a: Partial<A> = {}
+        const errors: Array<Tree<string>> = []
         for (const k in properties) {
           const e = properties[k].decode(r[k])
           if (isLeft(e)) {
-            return failures(`required property ${JSON.stringify(k)}`, e.left)
+            errors.push(tree(`required property ${JSON.stringify(k)}`, e.left))
           } else {
             a[k] = e.right
           }
         }
-        return success(a as A)
+        return isNotEmpty(errors) ? left(errors) : success(a as A)
       }
     }
   }
@@ -223,7 +218,8 @@ export function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decod
         return e
       } else {
         const r = e.right
-        let a: Partial<A> = {}
+        const a: Partial<A> = {}
+        const errors: Array<Tree<string>> = []
         for (const k in properties) {
           // don't add missing properties
           if (k in r) {
@@ -234,14 +230,14 @@ export function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decod
             } else {
               const e = properties[k].decode(rk)
               if (isLeft(e)) {
-                return failures(`optional property ${JSON.stringify(k)}`, e.left)
+                errors.push(tree(`optional property ${JSON.stringify(k)}`, e.left))
               } else {
                 a[k] = e.right
               }
             }
           }
         }
-        return success(a)
+        return isNotEmpty(errors) ? left(errors) : success(a)
       }
     }
   }
@@ -258,16 +254,17 @@ export function record<A>(codomain: Decoder<A>): Decoder<Record<string, A>> {
         return e
       } else {
         const r = e.right
-        let a: Record<string, A> = {}
+        const a: Record<string, A> = {}
+        const errors: Array<Tree<string>> = []
         for (const k in r) {
           const e = codomain.decode(r[k])
           if (isLeft(e)) {
-            return failures(`key ${JSON.stringify(k)}`, e.left)
+            errors.push(tree(`key ${JSON.stringify(k)}`, e.left))
           } else {
             a[k] = e.right
           }
         }
-        return success(a)
+        return isNotEmpty(errors) ? left(errors) : success(a)
       }
     }
   }
@@ -286,15 +283,16 @@ export function array<A>(items: Decoder<A>): Decoder<Array<A>> {
         const us = e.right
         const len = us.length
         const a: Array<A> = new Array(len)
+        const errors: Array<Tree<string>> = []
         for (let i = 0; i < len; i++) {
           const e = items.decode(us[i])
           if (isLeft(e)) {
-            return failures(`item ${i}`, e.left)
+            errors.push(tree(`item ${i}`, e.left))
           } else {
             a[i] = e.right
           }
         }
-        return success(a)
+        return isNotEmpty(errors) ? left(errors) : success(a)
       }
     }
   }
@@ -312,15 +310,16 @@ export function tuple<A extends ReadonlyArray<unknown>>(...components: { [K in k
       }
       const us = e.right
       const a: Array<unknown> = []
+      const errors: Array<Tree<string>> = []
       for (let i = 0; i < components.length; i++) {
         const e = components[i].decode(us[i])
         if (isLeft(e)) {
-          return failures(`component ${i}`, e.left)
+          errors.push(tree(`component ${i}`, e.left))
         } else {
           a.push(e.right)
         }
       }
-      return success(a as any)
+      return isNotEmpty(errors) ? left(errors) : success(a as any)
     }
   }
 }
@@ -398,8 +397,10 @@ export function sum<T extends string>(
         if (G.string.is(v) && v in members) {
           return (members as any)[v].decode(u)
         }
-        return failures(`required property ${JSON.stringify(tag)}`, [
-          tree(`cannot decode ${JSON.stringify(v)}, should be ${expected}`)
+        return left([
+          tree(`required property ${JSON.stringify(tag)}`, [
+            tree(`cannot decode ${JSON.stringify(v)}, should be ${expected}`)
+          ])
         ])
       }
     }


Test patch:
diff --git a/test/Codec.ts b/test/Codec.ts
index 0e5d89d..d2b96ca 100644
--- a/test/Codec.ts
+++ b/test/Codec.ts
@@ -215,6 +215,20 @@ describe('Codec', () => {
           left([D.tree('required property "a"', [D.tree('cannot decode 1, should be string')])])
         )
       })
+
+      it('should collect all errors', () => {
+        const codec = C.type({
+          a: C.string,
+          b: C.number
+        })
+        assert.deepStrictEqual(
+          codec.decode({}),
+          left([
+            D.tree('required property "a"', [D.tree('cannot decode undefined, should be string')]),
+            D.tree('required property "b"', [D.tree('cannot decode undefined, should be number')])
+          ])
+        )
+      })
     })
 
     describe('encode', () => {
@@ -265,6 +279,20 @@ describe('Codec', () => {
           left([D.tree('optional property "a"', [D.tree('cannot decode 1, should be string')])])
         )
       })
+
+      it('should collect all errors', () => {
+        const codec = C.partial({
+          a: C.string,
+          b: C.number
+        })
+        assert.deepStrictEqual(
+          codec.decode({ a: 1, b: 'b' }),
+          left([
+            D.tree('optional property "a"', [D.tree('cannot decode 1, should be string')]),
+            D.tree('optional property "b"', [D.tree('cannot decode "b", should be number')])
+          ])
+        )
+      })
     })
 
     describe('encode', () => {
@@ -311,6 +339,17 @@ describe('Codec', () => {
           left([D.tree('key "a"', [D.tree('cannot decode "a", should be number')])])
         )
       })
+
+      it('should collect all errors', () => {
+        const codec = C.record(C.number)
+        assert.deepStrictEqual(
+          codec.decode({ a: 'a', b: 'b' }),
+          left([
+            D.tree('key "a"', [D.tree('cannot decode "a", should be number')]),
+            D.tree('key "b"', [D.tree('cannot decode "b", should be number')])
+          ])
+        )
+      })
     })
 
     describe('encode', () => {
@@ -340,6 +379,17 @@ describe('Codec', () => {
           left([D.tree('item 0', [D.tree('cannot decode 1, should be string')])])
         )
       })
+
+      it('should collect all errors', () => {
+        const codec = C.array(C.string)
+        assert.deepStrictEqual(
+          codec.decode([1, 2]),
+          left([
+            D.tree('item 0', [D.tree('cannot decode 1, should be string')]),
+            D.tree('item 1', [D.tree('cannot decode 2, should be string')])
+          ])
+        )
+      })
     })
 
     describe('encode', () => {
@@ -377,6 +427,17 @@ describe('Codec', () => {
         )
       })
 
+      it('should collect all errors', () => {
+        const codec = C.tuple(C.string, C.number)
+        assert.deepStrictEqual(
+          codec.decode([1, 'a']),
+          left([
+            D.tree('component 0', [D.tree('cannot decode 1, should be string')]),
+            D.tree('component 1', [D.tree('cannot decode "a", should be number')])
+          ])
+        )
+      })
+
       it('should strip additional components', () => {
         const codec = C.tuple(C.string, C.number)
         assert.deepStrictEqual(codec.decode(['a', 1, true]), right(['a', 1]))
diff --git a/test/record.ts b/test/record.ts
index b93df4f..d0884fc 100644
--- a/test/record.ts
+++ b/test/record.ts
@@ -75,11 +75,6 @@ describe('record', () => {
         assertSuccess(T.decode({ a: 1 }))
       })
 
-      it.skip('should strip additional properties', () => {
-        const T = t.record(t.string, t.number)
-        assertSuccess(T.decode({ a: 1, b: 'b' }), { a: 1 })
-      })
-
       it('should return the same reference while decoding isomorphic values', () => {
         const T = t.record(t.string, t.number)
         const value1 = { a: 1 }