Repository: gcanti/io-ts
Pull request #482 (2020-06-16 11:09:09)

version 2.2.6


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c108d1d..e8a37c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,13 @@
 **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.6
+
+- **Experimental**
+  - `Decoder`
+    - add support for non-`string` tag values to `sum`, closes #481 (@gcanti)
+    - `intersection` should accumulate all errors (@gcanti)
+
 # 2.2.5
 
 - **Experimental**
diff --git a/package.json b/package.json
index a53acf2..6f0ce91 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "io-ts",
-  "version": "2.2.5",
+  "version": "2.2.6",
   "description": "TypeScript runtime type system for IO decoding/encoding",
   "files": [
     "lib",
diff --git a/src/Decoder.ts b/src/Decoder.ts
index 910fb10..f92eb4f 100644
--- a/src/Decoder.ts
+++ b/src/Decoder.ts
@@ -1,7 +1,7 @@
 /**
  * @since 2.2.0
  */
-import { Either, isLeft, isRight, left, mapLeft, right } from 'fp-ts/lib/Either'
+import * as E from 'fp-ts/lib/Either'
 import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'
 import { pipe } from 'fp-ts/lib/pipeable'
 import { Forest, Tree } from 'fp-ts/lib/Tree'
@@ -14,6 +14,8 @@ import { Alt1 } from 'fp-ts/lib/Alt'
 // model
 // -------------------------------------------------------------------------------------
 
+import Either = E.Either
+
 /**
  * @category model
  * @since 2.2.0
@@ -59,7 +61,7 @@ export function tree<A>(value: A, forest: Forest<A> = empty): Tree<A> {
  * @since 2.2.0
  */
 export function success<A>(a: A): Either<DecodeError, A> {
-  return right(a)
+  return E.right(a)
 }
 
 /**
@@ -67,7 +69,7 @@ export function success<A>(a: A): Either<DecodeError, A> {
  * @since 2.2.0
  */
 export function failure<A = never>(message: string): Either<DecodeError, A> {
-  return left([tree(message)])
+  return E.left([tree(message)])
 }
 
 /**
@@ -170,7 +172,7 @@ export function withExpected<A>(
     decode: (u) =>
       pipe(
         decoder.decode(u),
-        mapLeft((nea) => expected(u, nea))
+        E.mapLeft((nea) => expected(u, nea))
       )
   }
 }
@@ -187,7 +189,7 @@ export function refinement<A, B extends A>(
   return {
     decode: (u) => {
       const e = from.decode(u)
-      if (isLeft(e)) {
+      if (E.isLeft(e)) {
         return e
       }
       const a = e.right
@@ -204,11 +206,11 @@ export function parse<A, B>(from: Decoder<A>, parser: (a: A) => Either<string, B
   return {
     decode: (u) => {
       const e = from.decode(u)
-      if (isLeft(e)) {
+      if (E.isLeft(e)) {
         return e
       }
       const pe = parser(e.right)
-      if (isLeft(pe)) {
+      if (E.isLeft(pe)) {
         return failure(pe.left)
       }
       return pe
@@ -232,7 +234,7 @@ export function type<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decoder<
   return {
     decode: (u) => {
       const e = UnknownRecord.decode(u)
-      if (isLeft(e)) {
+      if (E.isLeft(e)) {
         return e
       } else {
         const r = e.right
@@ -240,13 +242,13 @@ export function type<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decoder<
         const errors: Array<Tree<string>> = []
         for (const k in properties) {
           const e = properties[k].decode(r[k])
-          if (isLeft(e)) {
+          if (E.isLeft(e)) {
             errors.push(tree(`required property ${JSON.stringify(k)}`, e.left))
           } else {
             a[k] = e.right
           }
         }
-        return isNotEmpty(errors) ? left(errors) : success(a as A)
+        return isNotEmpty(errors) ? E.left(errors) : success(a as A)
       }
     }
   }
@@ -260,7 +262,7 @@ export function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decod
   return {
     decode: (u) => {
       const e = UnknownRecord.decode(u)
-      if (isLeft(e)) {
+      if (E.isLeft(e)) {
         return e
       } else {
         const r = e.right
@@ -275,7 +277,7 @@ export function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decod
               a[k] = undefined
             } else {
               const e = properties[k].decode(rk)
-              if (isLeft(e)) {
+              if (E.isLeft(e)) {
                 errors.push(tree(`optional property ${JSON.stringify(k)}`, e.left))
               } else {
                 a[k] = e.right
@@ -283,7 +285,7 @@ export function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decod
             }
           }
         }
-        return isNotEmpty(errors) ? left(errors) : success(a)
+        return isNotEmpty(errors) ? E.left(errors) : success(a)
       }
     }
   }
@@ -297,7 +299,7 @@ export function record<A>(codomain: Decoder<A>): Decoder<Record<string, A>> {
   return {
     decode: (u) => {
       const e = UnknownRecord.decode(u)
-      if (isLeft(e)) {
+      if (E.isLeft(e)) {
         return e
       } else {
         const r = e.right
@@ -305,13 +307,13 @@ export function record<A>(codomain: Decoder<A>): Decoder<Record<string, A>> {
         const errors: Array<Tree<string>> = []
         for (const k in r) {
           const e = codomain.decode(r[k])
-          if (isLeft(e)) {
+          if (E.isLeft(e)) {
             errors.push(tree(`key ${JSON.stringify(k)}`, e.left))
           } else {
             a[k] = e.right
           }
         }
-        return isNotEmpty(errors) ? left(errors) : success(a)
+        return isNotEmpty(errors) ? E.left(errors) : success(a)
       }
     }
   }
@@ -325,7 +327,7 @@ export function array<A>(items: Decoder<A>): Decoder<Array<A>> {
   return {
     decode: (u) => {
       const e = UnknownArray.decode(u)
-      if (isLeft(e)) {
+      if (E.isLeft(e)) {
         return e
       } else {
         const us = e.right
@@ -334,13 +336,13 @@ export function array<A>(items: Decoder<A>): Decoder<Array<A>> {
         const errors: Array<Tree<string>> = []
         for (let i = 0; i < len; i++) {
           const e = items.decode(us[i])
-          if (isLeft(e)) {
+          if (E.isLeft(e)) {
             errors.push(tree(`item ${i}`, e.left))
           } else {
             a[i] = e.right
           }
         }
-        return isNotEmpty(errors) ? left(errors) : success(a)
+        return isNotEmpty(errors) ? E.left(errors) : success(a)
       }
     }
   }
@@ -354,7 +356,7 @@ export function tuple<A extends ReadonlyArray<unknown>>(...components: { [K in k
   return {
     decode: (u) => {
       const e = UnknownArray.decode(u)
-      if (isLeft(e)) {
+      if (E.isLeft(e)) {
         return e
       }
       const us = e.right
@@ -362,13 +364,13 @@ export function tuple<A extends ReadonlyArray<unknown>>(...components: { [K in k
       const errors: Array<Tree<string>> = []
       for (let i = 0; i < components.length; i++) {
         const e = components[i].decode(us[i])
-        if (isLeft(e)) {
+        if (E.isLeft(e)) {
           errors.push(tree(`component ${i}`, e.left))
         } else {
           a.push(e.right)
         }
       }
-      return isNotEmpty(errors) ? left(errors) : success(a as any)
+      return isNotEmpty(errors) ? E.left(errors) : success(a as any)
     }
   }
 }
@@ -399,11 +401,11 @@ export function intersection<A, B>(left: Decoder<A>, right: Decoder<B>): Decoder
   return {
     decode: (u) => {
       const ea = left.decode(u)
-      if (isLeft(ea)) {
-        return ea
-      }
       const eb = right.decode(u)
-      if (isLeft(eb)) {
+      if (E.isLeft(ea)) {
+        return E.isLeft(eb) ? E.left(ea.left.concat(eb.left) as DecodeError) : ea
+      }
+      if (E.isLeft(eb)) {
         return eb
       }
       return success(intersect(ea.right, eb.right))
@@ -421,7 +423,7 @@ export function lazy<A>(id: string, f: () => Decoder<A>): Decoder<A> {
     decode: (u) =>
       pipe(
         get().decode(u),
-        mapLeft((nea) => [tree(id, nea)])
+        E.mapLeft((nea) => [tree(id, nea)])
       )
   }
 }
@@ -431,7 +433,7 @@ export function lazy<A>(id: string, f: () => Decoder<A>): Decoder<A> {
  * @since 2.2.0
  */
 export function sum<T extends string>(tag: T): <A>(members: { [K in keyof A]: Decoder<A[K]> }) => Decoder<A[keyof A]> {
-  return (members) => {
+  return <A>(members: { [K in keyof A]: Decoder<A[K]> }) => {
     const keys = Object.keys(members)
     if (keys.length === 0) {
       return never
@@ -440,14 +442,14 @@ export function sum<T extends string>(tag: T): <A>(members: { [K in keyof A]: De
     return {
       decode: (u) => {
         const e = UnknownRecord.decode(u)
-        if (isLeft(e)) {
+        if (E.isLeft(e)) {
           return e
         }
-        const v = e.right[tag]
-        if (G.string.is(v) && v in members) {
-          return (members as any)[v].decode(u)
+        const v = e.right[tag] as keyof A
+        if (v in members) {
+          return members[v].decode(u)
         }
-        return left([
+        return E.left([
           tree(`required property ${JSON.stringify(tag)}`, [
             tree(`cannot decode ${JSON.stringify(v)}, should be ${expected}`)
           ])
@@ -471,19 +473,19 @@ export function union<A extends ReadonlyArray<unknown>>(
   return {
     decode: (u) => {
       const e = members[0].decode(u)
-      if (isRight(e)) {
+      if (E.isRight(e)) {
         return e
       } else {
         const errors: DecodeError = [tree(`member 0`, e.left)]
         for (let i = 1; i < len; i++) {
           const e = members[i].decode(u)
-          if (isRight(e)) {
+          if (E.isRight(e)) {
             return e
           } else {
             errors.push(tree(`member ${i}`, e.left))
           }
         }
-        return left(errors)
+        return E.left(errors)
       }
     }
   }
@@ -502,7 +504,7 @@ export const map: <A, B>(f: (a: A) => B) => (fa: Decoder<A>) => Decoder<B> = (f)
 const map_: <A, B>(fa: Decoder<A>, f: (a: A) => B) => Decoder<B> = (fa, f) => ({
   decode: (u) => {
     const e = fa.decode(u)
-    return isLeft(e) ? e : right(f(e.right))
+    return E.isLeft(e) ? e : E.right(f(e.right))
   }
 })
 
@@ -515,7 +517,7 @@ export const alt: <A>(that: () => Decoder<A>) => (fa: Decoder<A>) => Decoder<A>
 const alt_: <A>(fx: Decoder<A>, fy: () => Decoder<A>) => Decoder<A> = (fx, fy) => ({
   decode: (u) => {
     const e = fx.decode(u)
-    return isLeft(e) ? fy().decode(u) : e
+    return E.isLeft(e) ? fy().decode(u) : e
   }
 })
 
diff --git a/src/Guard.ts b/src/Guard.ts
index e735c31..da54a27 100644
--- a/src/Guard.ts
+++ b/src/Guard.ts
@@ -205,10 +205,10 @@ export function union<A extends ReadonlyArray<unknown>>(...members: { [K in keyo
  * @since 2.2.0
  */
 export function sum<T extends string>(tag: T): <A>(members: { [K in keyof A]: Guard<A[K]> }) => Guard<A[keyof A]> {
-  return (members: Record<string, Guard<unknown>>) =>
+  return <A>(members: { [K in keyof A]: Guard<A[K]> }) =>
     refinement(UnknownRecord, (r): r is any => {
-      const v = r[tag]
-      if (string.is(v) && v in members) {
+      const v = r[tag] as keyof A
+      if (v in members) {
         return members[v].is(r)
       }
       return false


Test patch:
diff --git a/test/Codec.ts b/test/Codec.ts
index c9c30e1..7b3809e 100644
--- a/test/Codec.ts
+++ b/test/Codec.ts
@@ -501,18 +501,6 @@ describe('Codec', () => {
         const codec = C.intersection(Int, Positive)
         assert.deepStrictEqual(codec.decode(1), right(1))
       })
-
-      it('should reject an invalid input', () => {
-        const codec = C.intersection(C.type({ a: C.string }), C.type({ b: C.number }))
-        assert.deepStrictEqual(
-          codec.decode({ a: 'a' }),
-          left([D.tree('required property "b"', [D.tree('cannot decode undefined, should be number')])])
-        )
-        assert.deepStrictEqual(
-          codec.decode({ b: 1 }),
-          left([D.tree('required property "a"', [D.tree('cannot decode undefined, should be string')])])
-        )
-      })
     })
 
     describe('encode', () => {
diff --git a/test/Decoder.ts b/test/Decoder.ts
index 80ed42c..81f762e 100644
--- a/test/Decoder.ts
+++ b/test/Decoder.ts
@@ -55,6 +55,42 @@ describe('Decoder', () => {
     })
   })
 
+  describe('sum', () => {
+    it('should support non-`string` tag values', () => {
+      const decoder = D.sum('_tag')({
+        true: D.type({ _tag: D.literal(true), a: D.string }),
+        false: D.type({ _tag: D.literal(false), b: D.number })
+      })
+      assert.deepStrictEqual(decoder.decode({ _tag: true, a: 'a' }), E.right({ _tag: true, a: 'a' }))
+      assert.deepStrictEqual(decoder.decode({ _tag: false, b: 1 }), E.right({ _tag: false, b: 1 }))
+      assert.deepStrictEqual(
+        decoder.decode({ _tag: false, b: 'a' }),
+        E.left([D.tree('required property "b"', [D.tree('cannot decode "a", should be number')])])
+      )
+    })
+  })
+
+  describe('intersection', () => {
+    it('should accumulate all errors', () => {
+      const decoder = D.intersection(D.type({ a: D.string }), D.type({ b: D.number }))
+      assert.deepStrictEqual(
+        decoder.decode({}),
+        E.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')])
+        ])
+      )
+      assert.deepStrictEqual(
+        decoder.decode({ b: 1 }),
+        E.left([D.tree('required property "a"', [D.tree('cannot decode undefined, should be string')])])
+      )
+      assert.deepStrictEqual(
+        decoder.decode({ a: 'a' }),
+        E.left([D.tree('required property "b"', [D.tree('cannot decode undefined, should be number')])])
+      )
+    })
+  })
+
   describe('intersect', () => {
     it('should concat strings', () => {
       assert.deepStrictEqual(D.intersect('a', 'b'), 'b')
diff --git a/test/Guard.ts b/test/Guard.ts
index 4c910e7..6847264 100644
--- a/test/Guard.ts
+++ b/test/Guard.ts
@@ -230,5 +230,15 @@ describe('Guard', () => {
       assert.deepStrictEqual(sum({}).is(null), false)
       assert.deepStrictEqual(sum({}).is({}), false)
     })
+
+    it('should support non-`string` tag values', () => {
+      const guard = G.sum('_tag')({
+        true: G.type({ _tag: G.literal(true), a: G.string }),
+        false: G.type({ _tag: G.literal(false), b: G.number })
+      })
+      assert.deepStrictEqual(guard.is({ _tag: true, a: 'a' }), true)
+      assert.deepStrictEqual(guard.is({ _tag: false, b: 1 }), true)
+      assert.deepStrictEqual(guard.is({ _tag: false, b: 'a' }), false)
+    })
   })
 })
diff --git a/test/Schema.ts b/test/Schema.ts
index 1f95324..027e35a 100644
--- a/test/Schema.ts
+++ b/test/Schema.ts
@@ -121,12 +121,4 @@ describe('Schema', () => {
     )
     check(schema)
   })
-
-  // it('UnknownArray', () => {
-  //   check(make((S) => S.UnknownArray))
-  // })
-
-  // it('UnknownRecord', () => {
-  //   check(make((S) => S.UnknownRecord))
-  // })
 })