Repository: gcanti/io-ts
Pull request #500 (2020-07-16 08:52:25)

version 2.2.9


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3243ef6..3ba7d9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,18 @@
 **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.9
+
+- **Polish**
+  - remove `Object.freeze` calls, closes #497 (@gcanti)
+- **Experimental**
+  - `DecodeError`
+    - add `Wrap` member (@gcanti)
+  - `Decoder`
+    - add `withMessage` combinator (@gcanti)
+  - `TaskDecoder`
+    - add `withMessage` combinator (@gcanti)
+
 # 2.2.8
 
 - **Experimental**
diff --git a/docs/modules/DecodeError.ts.md b/docs/modules/DecodeError.ts.md
index a104745..1725e4f 100644
--- a/docs/modules/DecodeError.ts.md
+++ b/docs/modules/DecodeError.ts.md
@@ -25,6 +25,7 @@ Added in v2.2.7
   - [lazy](#lazy)
   - [leaf](#leaf)
   - [member](#member)
+  - [wrap](#wrap)
 - [destructors](#destructors)
   - [fold](#fold)
 - [instances](#instances)
@@ -37,6 +38,7 @@ Added in v2.2.7
   - [Lazy (interface)](#lazy-interface)
   - [Leaf (interface)](#leaf-interface)
   - [Member (interface)](#member-interface)
+  - [Wrap (interface)](#wrap-interface)
   - [optional](#optional)
   - [required](#required)
 
@@ -94,6 +96,16 @@ export declare const member: <E>(index: number, errors: FS.FreeSemigroup<DecodeE
 
 Added in v2.2.7
 
+## wrap
+
+**Signature**
+
+```ts
+export declare const wrap: <E>(error: E, errors: FS.FreeSemigroup<DecodeError<E>>) => DecodeError<E>
+```
+
+Added in v2.2.9
+
 # destructors
 
 ## fold
@@ -107,6 +119,7 @@ export declare const fold: <E, R>(patterns: {
   Index: (index: number, kind: Kind, errors: FS.FreeSemigroup<DecodeError<E>>) => R
   Member: (index: number, errors: FS.FreeSemigroup<DecodeError<E>>) => R
   Lazy: (id: string, errors: FS.FreeSemigroup<DecodeError<E>>) => R
+  Wrap: (error: E, errors: FS.FreeSemigroup<DecodeError<E>>) => R
 }) => (e: DecodeError<E>) => R
 ```
 
@@ -131,7 +144,7 @@ Added in v2.2.7
 **Signature**
 
 ```ts
-export type DecodeError<E> = Leaf<E> | Key<E> | Index<E> | Member<E> | Lazy<E>
+export type DecodeError<E> = Leaf<E> | Key<E> | Index<E> | Member<E> | Lazy<E> | Wrap<E>
 ```
 
 Added in v2.2.7
@@ -218,6 +231,20 @@ export interface Member<E> {
 
 Added in v2.2.7
 
+## Wrap (interface)
+
+**Signature**
+
+```ts
+export interface Wrap<E> {
+  readonly _tag: 'Wrap'
+  readonly error: E
+  readonly errors: FS.FreeSemigroup<DecodeError<E>>
+}
+```
+
+Added in v2.2.9
+
 ## optional
 
 **Signature**
diff --git a/docs/modules/Decoder.ts.md b/docs/modules/Decoder.ts.md
index e465e63..e4df5be 100644
--- a/docs/modules/Decoder.ts.md
+++ b/docs/modules/Decoder.ts.md
@@ -52,6 +52,7 @@ Added in v2.2.7
   - [tuple](#tuple)
   - [type](#type)
   - [union](#union)
+  - [withMessage](#withmessage)
 - [constructors](#constructors)
   - [fromGuard](#fromguard)
   - [fromRefinement](#fromrefinement)
@@ -398,6 +399,18 @@ export declare const union: <MS extends readonly [Decoder<any, any>, ...Decoder<
 
 Added in v2.2.7
 
+## withMessage
+
+**Signature**
+
+```ts
+export declare const withMessage: <I>(
+  message: (input: I, e: FS.FreeSemigroup<DE.DecodeError<string>>) => string
+) => <A>(decoder: Decoder<I, A>) => Decoder<I, A>
+```
+
+Added in v2.2.9
+
 # constructors
 
 ## fromGuard
diff --git a/docs/modules/TaskDecoder.ts.md b/docs/modules/TaskDecoder.ts.md
index 0465d0c..0d7c789 100644
--- a/docs/modules/TaskDecoder.ts.md
+++ b/docs/modules/TaskDecoder.ts.md
@@ -52,6 +52,7 @@ Added in v2.2.7
   - [tuple](#tuple)
   - [type](#type)
   - [union](#union)
+  - [withMessage](#withmessage)
 - [constructors](#constructors)
   - [fromDecoder](#fromdecoder)
   - [fromGuard](#fromguard)
@@ -401,6 +402,18 @@ export declare const union: <MS extends readonly [TaskDecoder<any, any>, ...Task
 
 Added in v2.2.7
 
+## withMessage
+
+**Signature**
+
+```ts
+export declare const withMessage: <I>(
+  message: (input: I, e: FS.FreeSemigroup<DE.DecodeError<string>>) => string
+) => <A>(decoder: TaskDecoder<I, A>) => TaskDecoder<I, A>
+```
+
+Added in v2.2.9
+
 # constructors
 
 ## fromDecoder
diff --git a/docs/modules/index.ts.md b/docs/modules/index.ts.md
index 393ec50..0ff850a 100644
--- a/docs/modules/index.ts.md
+++ b/docs/modules/index.ts.md
@@ -218,7 +218,7 @@ Added in v1.0.0
 **Signature**
 
 ```ts
-export declare const array: <C extends Mixed>(codec: C, name?: string) => ArrayC<C>
+export declare const array: <C extends Mixed>(item: C, name?: string) => ArrayC<C>
 ```
 
 Added in v1.0.0
@@ -310,7 +310,7 @@ Added in v1.0.0
 **Signature**
 
 ```ts
-export declare const readonlyArray: <C extends Mixed>(codec: C, name?: string) => ReadonlyArrayC<C>
+export declare const readonlyArray: <C extends Mixed>(item: C, name?: string) => ReadonlyArrayC<C>
 ```
 
 Added in v1.0.0
diff --git a/package.json b/package.json
index 2b7ee7b..fe4e9a7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "io-ts",
-  "version": "2.2.8",
+  "version": "2.2.9",
   "description": "TypeScript runtime type system for IO decoding/encoding",
   "files": [
     "lib",
diff --git a/src/DecodeError.ts b/src/DecodeError.ts
index 37b8c59..dac9744 100644
--- a/src/DecodeError.ts
+++ b/src/DecodeError.ts
@@ -81,11 +81,21 @@ export interface Lazy<E> {
   readonly errors: FS.FreeSemigroup<DecodeError<E>>
 }
 
+/**
+ * @category model
+ * @since 2.2.9
+ */
+export interface Wrap<E> {
+  readonly _tag: 'Wrap'
+  readonly error: E
+  readonly errors: FS.FreeSemigroup<DecodeError<E>>
+}
+
 /**
  * @category model
  * @since 2.2.7
  */
-export type DecodeError<E> = Leaf<E> | Key<E> | Index<E> | Member<E> | Lazy<E>
+export type DecodeError<E> = Leaf<E> | Key<E> | Index<E> | Member<E> | Lazy<E> | Wrap<E>
 
 /**
  * @category constructors
@@ -135,6 +145,16 @@ export const lazy = <E>(id: string, errors: FS.FreeSemigroup<DecodeError<E>>): D
   errors
 })
 
+/**
+ * @category constructors
+ * @since 2.2.9
+ */
+export const wrap = <E>(error: E, errors: FS.FreeSemigroup<DecodeError<E>>): DecodeError<E> => ({
+  _tag: 'Wrap',
+  error,
+  errors
+})
+
 /**
  * @category destructors
  * @since 2.2.7
@@ -145,6 +165,7 @@ export const fold = <E, R>(patterns: {
   Index: (index: number, kind: Kind, errors: FS.FreeSemigroup<DecodeError<E>>) => R
   Member: (index: number, errors: FS.FreeSemigroup<DecodeError<E>>) => R
   Lazy: (id: string, errors: FS.FreeSemigroup<DecodeError<E>>) => R
+  Wrap: (error: E, errors: FS.FreeSemigroup<DecodeError<E>>) => R
 }): ((e: DecodeError<E>) => R) => {
   const f = (e: DecodeError<E>): R => {
     switch (e._tag) {
@@ -158,6 +179,8 @@ export const fold = <E, R>(patterns: {
         return patterns.Member(e.index, e.errors)
       case 'Lazy':
         return patterns.Lazy(e.id, e.errors)
+      case 'Wrap':
+        return patterns.Wrap(e.error, e.errors)
     }
   }
   return f
diff --git a/src/Decoder.ts b/src/Decoder.ts
index 13e2496..51d97db 100644
--- a/src/Decoder.ts
+++ b/src/Decoder.ts
@@ -192,6 +192,15 @@ export const mapLeftWithInput: <I>(
   /*#__PURE__*/
   K.mapLeftWithInput(M)
 
+/**
+ * @category combinators
+ * @since 2.2.9
+ */
+export const withMessage = <I>(
+  message: (input: I, e: DecodeError) => string
+): (<A>(decoder: Decoder<I, A>) => Decoder<I, A>) =>
+  mapLeftWithInput((input, e) => FS.of(DE.wrap(message(input, e), e)))
+
 /**
  * @category combinators
  * @since 2.2.7
@@ -536,7 +545,8 @@ const toTree: (e: DE.DecodeError<string>) => Tree<string> = DE.fold({
   Key: (key, kind, errors) => make(`${kind} property ${JSON.stringify(key)}`, toForest(errors)),
   Index: (index, kind, errors) => make(`${kind} index ${index}`, toForest(errors)),
   Member: (index, errors) => make(`member ${index}`, toForest(errors)),
-  Lazy: (id, errors) => make(`lazy type ${id}`, toForest(errors))
+  Lazy: (id, errors) => make(`lazy type ${id}`, toForest(errors)),
+  Wrap: (error, errors) => make(error, toForest(errors))
 })
 
 const toForest: (e: DecodeError) => ReadonlyArray<Tree<string>> = FS.fold(
diff --git a/src/TaskDecoder.ts b/src/TaskDecoder.ts
index 7e80406..d8ef66d 100644
--- a/src/TaskDecoder.ts
+++ b/src/TaskDecoder.ts
@@ -193,6 +193,15 @@ export const mapLeftWithInput: <I>(
   /*#__PURE__*/
   K.mapLeftWithInput(M)
 
+/**
+ * @category combinators
+ * @since 2.2.9
+ */
+export const withMessage = <I>(
+  message: (input: I, e: DecodeError) => string
+): (<A>(decoder: TaskDecoder<I, A>) => TaskDecoder<I, A>) =>
+  mapLeftWithInput((input, e) => FS.of(DE.wrap(message(input, e), e)))
+
 /**
  * @category combinators
  * @since 2.2.7
diff --git a/src/index.ts b/src/index.ts
index 09af4c6..10903a6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -793,10 +793,10 @@ export interface ArrayC<C extends Mixed> extends ArrayType<C, Array<TypeOf<C>>,
  * @category Combinators
  * @since 1.0.0
  */
-export const array = <C extends Mixed>(codec: C, name: string = `Array<${codec.name}>`): ArrayC<C> =>
+export const array = <C extends Mixed>(item: C, name: string = `Array<${item.name}>`): ArrayC<C> =>
   new ArrayType(
     name,
-    (u): u is Array<TypeOf<C>> => UnknownArray.is(u) && u.every(codec.is),
+    (u): u is Array<TypeOf<C>> => UnknownArray.is(u) && u.every(item.is),
     (u, c) => {
       const e = UnknownArray.validate(u, c)
       if (isLeft(e)) {
@@ -808,7 +808,7 @@ export const array = <C extends Mixed>(codec: C, name: string = `Array<${codec.n
       const errors: Errors = []
       for (let i = 0; i < len; i++) {
         const ui = us[i]
-        const result = codec.validate(ui, appendContext(c, String(i), codec, ui))
+        const result = item.validate(ui, appendContext(c, String(i), item, ui))
         if (isLeft(result)) {
           pushAll(errors, result.left)
         } else {
@@ -823,8 +823,8 @@ export const array = <C extends Mixed>(codec: C, name: string = `Array<${codec.n
       }
       return errors.length > 0 ? failures(errors) : success(as)
     },
-    codec.encode === identity ? identity : (a) => a.map(codec.encode),
-    codec
+    item.encode === identity ? identity : (a) => a.map(item.encode),
+    item
   )
 
 /**
@@ -1633,23 +1633,7 @@ export interface ReadonlyC<C extends Mixed>
  * @since 1.0.0
  */
 export const readonly = <C extends Mixed>(codec: C, name: string = `Readonly<${codec.name}>`): ReadonlyC<C> => {
-  return new ReadonlyType(
-    name,
-    codec.is,
-    (u, c) => {
-      const e = codec.validate(u, c)
-      if (isLeft(e)) {
-        return e
-      }
-      const x = e.right
-      if (process.env.NODE_ENV !== 'production') {
-        return right(Object.freeze(x))
-      }
-      return right(x)
-    },
-    codec.encode === identity ? identity : codec.encode,
-    codec
-  )
+  return new ReadonlyType(name, codec.is, codec.validate, codec.encode, codec)
 }
 
 /**
@@ -1682,27 +1666,11 @@ export interface ReadonlyArrayC<C extends Mixed>
  * @since 1.0.0
  */
 export const readonlyArray = <C extends Mixed>(
-  codec: C,
-  name: string = `ReadonlyArray<${codec.name}>`
+  item: C,
+  name: string = `ReadonlyArray<${item.name}>`
 ): ReadonlyArrayC<C> => {
-  const arrayType = array(codec)
-  return new ReadonlyArrayType(
-    name,
-    arrayType.is,
-    (u, c) => {
-      const e = arrayType.validate(u, c)
-      if (isLeft(e)) {
-        return e
-      }
-      const x = e.right
-      if (process.env.NODE_ENV !== 'production') {
-        return right(Object.freeze(x))
-      }
-      return right(x)
-    },
-    arrayType.encode as any,
-    codec
-  )
+  const codec = array(item)
+  return new ReadonlyArrayType(name, codec.is, codec.validate, codec.encode, item) as any
 }
 
 /**


Test patch:
diff --git a/test/2.1.x/readonly.ts b/test/2.1.x/readonly.ts
index 42f6100..8a8d480 100644
--- a/test/2.1.x/readonly.ts
+++ b/test/2.1.x/readonly.ts
@@ -1,7 +1,6 @@
 import * as assert from 'assert'
 import * as t from '../../src/index'
-import { assertSuccess, assertFailure, NumberFromString } from './helpers'
-import { either } from 'fp-ts/lib/Either'
+import { assertFailure, assertSuccess, NumberFromString } from './helpers'
 
 describe('readonly', () => {
   describe('name', () => {
@@ -42,19 +41,6 @@ describe('readonly', () => {
       const T = t.readonly(t.type({ a: t.number }))
       assertFailure(T, {}, ['Invalid value undefined supplied to : Readonly<{ a: number }>/a: number'])
     })
-
-    it('should freeze the value', () => {
-      const T = t.readonly(t.type({ a: t.number }))
-      either.map(T.decode({ a: 1 }), (x) => assert.ok(Object.isFrozen(x)))
-    })
-
-    it('should not freeze in production', () => {
-      const env = process.env.NODE_ENV
-      process.env.NODE_ENV = 'production'
-      const T = t.readonly(t.type({ a: t.number }))
-      either.map(T.decode({ a: 1 }), (x) => assert.ok(!Object.isFrozen(x)))
-      process.env.NODE_ENV = env
-    })
   })
 
   describe('encode', () => {
diff --git a/test/2.1.x/readonlyArray.ts b/test/2.1.x/readonlyArray.ts
index 71ccd09..9920412 100644
--- a/test/2.1.x/readonlyArray.ts
+++ b/test/2.1.x/readonlyArray.ts
@@ -1,7 +1,6 @@
 import * as assert from 'assert'
 import * as t from '../../src/index'
-import { assertSuccess, assertFailure, NumberFromString } from './helpers'
-import { either } from 'fp-ts/lib/Either'
+import { assertFailure, assertSuccess, NumberFromString } from './helpers'
 
 describe('readonlyArray', () => {
   describe('name', () => {
@@ -44,19 +43,6 @@ describe('readonlyArray', () => {
       const T = t.readonlyArray(t.number)
       assertFailure(T, ['s'], ['Invalid value "s" supplied to : ReadonlyArray<number>/0: number'])
     })
-
-    it('should freeze the value', () => {
-      const T = t.readonlyArray(t.number)
-      either.map(T.decode([1]), (x) => assert.ok(Object.isFrozen(x)))
-    })
-
-    it('should not freeze in production', () => {
-      const env = process.env.NODE_ENV
-      process.env.NODE_ENV = 'production'
-      const T = t.readonlyArray(t.number)
-      either.map(T.decode([1]), (x) => assert.ok(!Object.isFrozen(x)))
-      process.env.NODE_ENV = env
-    })
   })
 
   describe('encode', () => {
diff --git a/test/Decoder.ts b/test/Decoder.ts
index a4f8f60..0be76e0 100644
--- a/test/Decoder.ts
+++ b/test/Decoder.ts
@@ -92,6 +92,24 @@ describe('Decoder', () => {
     assert.deepStrictEqual(decoder.decode('a'), E.left(FS.of(DE.leaf('a', 'not a number'))))
   })
 
+  it('withMessage', () => {
+    const decoder = pipe(
+      _.type({
+        name: _.string,
+        age: _.number
+      }),
+      _.withMessage(() => 'Person')
+    )
+    assert.deepStrictEqual(
+      pipe(decoder.decode({}), E.mapLeft(_.draw)),
+      E.left(`Person
+├─ required property "name"
+│  └─ cannot decode undefined, should be string
+└─ required property "age"
+   └─ cannot decode undefined, should be number`)
+    )
+  })
+
   it('compose', () => {
     interface IntBrand {
       readonly Int: unique symbol
diff --git a/test/TaskDecoder.ts b/test/TaskDecoder.ts
index f6ecef6..aaafd27 100644
--- a/test/TaskDecoder.ts
+++ b/test/TaskDecoder.ts
@@ -135,6 +135,24 @@ describe('UnknownTaskDecoder', () => {
     assert.deepStrictEqual(await decoder.decode('a')(), D.failure('a', 'not a number'))
   })
 
+  it('withMessage', async () => {
+    const decoder = pipe(
+      _.type({
+        name: _.string,
+        age: _.number
+      }),
+      _.withMessage(() => 'Person')
+    )
+    assert.deepStrictEqual(
+      await pipe(decoder.decode({}), TE.mapLeft(_.draw))(),
+      E.left(`Person
+├─ required property "name"
+│  └─ cannot decode undefined, should be string
+└─ required property "age"
+   └─ cannot decode undefined, should be number`)
+    )
+  })
+
   it('compose', async () => {
     interface IntBrand {
       readonly Int: unique symbol