Repository: gcanti/io-ts (ts)

Issue:
`record` with a `literal` key fails to decode inside of an `intersection`
### Current Behavior

```ts
const C = t.intersection([
	t.record(t.literal('funky'), t.string),
	t.record(t.string, t.unknown)
])

type C_ = t.TypeOf<typeof C>

/*
type C_ = {
    funky: string;
} & {
    [x: string]: unknown;
}
*/

const lit: C_ = {
	funky: 'key',
	more: 'nope'
} // Compiles fine


C.decode({
	funky: 'hey',
	more: 'nope'
}) // Left
```

### Expected behavior

```ts
C.decode({
	funky: 'hey',
	more: 'nope'
}) // Right
```

<!-- PLEASE FILL THIS OUT -->

| Software   | Version(s) |
| ---------- | ---------- |
| io-ts      | 2.0.1           |
| fp-ts      | 2.1.2      |
| TypeScript | 3.7.2       |


Pull request:
fix `record` domain handling, closes #391

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fffc846..da6166a 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.1.1
+
+- **Bug Fix**
+  - fix `record` domain handling, closes #391 (@gcanti)
+
 # 2.1.0
 
 - **New Feature**
diff --git a/docs/modules/index.ts.md b/docs/modules/index.ts.md
index c5704d3..059a480 100644
--- a/docs/modules/index.ts.md
+++ b/docs/modules/index.ts.md
@@ -1882,11 +1882,7 @@ Added in v1.0.0
 **Signature**
 
 ```ts
-export const record = <D extends Mixed, C extends Mixed>(
-  domain: D,
-  codomain: C,
-  name: string = `{ [K in ${domain.name}]: ${codomain.name} }`
-): RecordC<D, C> => ...
+export function record<D extends Mixed, C extends Mixed>(domain: D, codomain: C, name?: string): RecordC<D, C> { ... }
 ```
 
 Added in v1.7.1
diff --git a/package.json b/package.json
index 1607271..d7cac00 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "io-ts",
-  "version": "2.1.0",
+  "version": "2.1.1",
   "description": "TypeScript compatible runtime type system for IO validation",
   "files": [
     "lib",
diff --git a/src/index.ts b/src/index.ts
index 32bfdd8..d12a85a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -213,15 +213,6 @@ const pushAll = <A>(xs: Array<A>, ys: Array<A>): void => {
   }
 }
 
-const getIsCodec = <T extends Any>(tag: string) => (codec: Any): codec is T => (codec as any)._tag === tag
-
-// tslint:disable-next-line: deprecation
-const isAnyCodec = getIsCodec<AnyType>('AnyType')
-
-const isInterfaceCodec = getIsCodec<InterfaceType<Props>>('InterfaceType')
-
-const isPartialCodec = getIsCodec<PartialType<Props>>('PartialType')
-
 //
 // basic types
 //
@@ -999,21 +990,80 @@ export type OutputOfDictionary<D extends Any, C extends Any> = { [K in OutputOf<
 export interface RecordC<D extends Mixed, C extends Mixed>
   extends DictionaryType<D, C, { [K in TypeOf<D>]: TypeOf<C> }, { [K in OutputOf<D>]: OutputOf<C> }, unknown> {}
 
+function enumerableRecord<D extends Mixed, C extends Mixed>(
+  keys: Array<string>,
+  domain: D,
+  codomain: C,
+  name: string = `{ [K in ${domain.name}]: ${codomain.name} }`
+): RecordC<D, C> {
+  const len = keys.length
+  return new DictionaryType(
+    name,
+    (u): u is { [K in TypeOf<D>]: TypeOf<C> } => UnknownRecord.is(u) && keys.every(k => codomain.is(u[k])),
+    (u, c) =>
+      chain(UnknownRecord.validate(u, c), o => {
+        const a: { [key: string]: any } = {}
+        const errors: Errors = []
+        let changed: boolean = false
+        for (let i = 0; i < len; i++) {
+          const k = keys[i]
+          const ok = o[k]
+          const codomainResult = codomain.validate(ok, appendContext(c, k, codomain, ok))
+          if (isLeft(codomainResult)) {
+            pushAll(errors, codomainResult.left)
+          } else {
+            const vok = codomainResult.right
+            changed = changed || vok !== ok
+            a[k] = vok
+          }
+        }
+        return errors.length > 0 ? failures(errors) : success((changed || Object.keys(o).length !== len ? a : o) as any)
+      }),
+    codomain.encode === identity
+      ? identity
+      : (a: any) => {
+          const s: { [key: string]: any } = {}
+          for (let i = 0; i < len; i++) {
+            const k = keys[i]
+            s[k] = codomain.encode(a[k])
+          }
+          return s as any
+        },
+    domain,
+    codomain
+  )
+}
+
 /**
- * @since 1.7.1
+ * @internal
  */
-export const record = <D extends Mixed, C extends Mixed>(
+export function getDomainKeys<D extends Mixed>(domain: D): Record<string, unknown> | undefined {
+  if (isLiteralC(domain)) {
+    const literal = domain.value
+    if (string.is(literal)) {
+      return { [literal]: null }
+    }
+  } else if (isKeyofC(domain)) {
+    return domain.keys
+  } else if (isUnionC(domain)) {
+    const keys = domain.types.map(type => getDomainKeys(type))
+    return keys.some(undefinedType.is) ? undefined : Object.assign({}, ...keys)
+  }
+  return undefined
+}
+
+function nonEnumerableRecord<D extends Mixed, C extends Mixed>(
   domain: D,
   codomain: C,
   name: string = `{ [K in ${domain.name}]: ${codomain.name} }`
-): RecordC<D, C> => {
+): RecordC<D, C> {
   return new DictionaryType(
     name,
     (u): u is { [K in TypeOf<D>]: TypeOf<C> } => {
       if (UnknownRecord.is(u)) {
         return Object.keys(u).every(k => domain.is(k) && codomain.is(u[k]))
       }
-      return isAnyCodec(codomain) && Array.isArray(u)
+      return isAnyC(codomain) && Array.isArray(u)
     },
     (u, c) => {
       if (UnknownRecord.is(u)) {
@@ -1044,7 +1094,7 @@ export const record = <D extends Mixed, C extends Mixed>(
         }
         return errors.length > 0 ? failures(errors) : success((changed ? a : u) as any)
       }
-      if (isAnyCodec(codomain) && Array.isArray(u)) {
+      if (isAnyC(codomain) && Array.isArray(u)) {
         return success(u)
       }
       return failure(u, c)
@@ -1066,6 +1116,16 @@ export const record = <D extends Mixed, C extends Mixed>(
   )
 }
 
+/**
+ * @since 1.7.1
+ */
+export function record<D extends Mixed, C extends Mixed>(domain: D, codomain: C, name?: string): RecordC<D, C> {
+  const keys = getDomainKeys(domain)
+  return keys
+    ? enumerableRecord(Object.keys(keys), domain, codomain, name)
+    : nonEnumerableRecord(domain, codomain, name)
+}
+
 /**
  * @since 1.0.0
  */
@@ -1628,9 +1688,9 @@ const stripKeys = (o: any, props: Props): unknown => {
 }
 
 const getExactTypeName = (codec: Any): string => {
-  if (isInterfaceCodec(codec)) {
+  if (isTypeC(codec)) {
     return `{| ${getNameFromProps(codec.props)} |}`
-  } else if (isPartialCodec(codec)) {
+  } else if (isPartialC(codec)) {
     return getPartialTypeName(`{| ${getNameFromProps(codec.props)} |}`)
   }
   return `Exact<${codec.name}>`
@@ -2109,14 +2169,27 @@ function intersectTags(a: Tags, b: Tags): Tags {
   return r
 }
 
+// tslint:disable-next-line: deprecation
+function isAnyC(codec: Any): codec is AnyC {
+  return (codec as any)._tag === 'AnyType'
+}
+
 function isLiteralC(codec: Any): codec is LiteralC<LiteralValue> {
   return (codec as any)._tag === 'LiteralType'
 }
 
+function isKeyofC(codec: Any): codec is KeyofC<Record<string, unknown>> {
+  return (codec as any)._tag === 'KeyofType'
+}
+
 function isTypeC(codec: Any): codec is TypeC<Props> {
   return (codec as any)._tag === 'InterfaceType'
 }
 
+function isPartialC(codec: Any): codec is PartialC<Props> {
+  return (codec as any)._tag === 'PartialType'
+}
+
 // tslint:disable-next-line: deprecation
 function isStrictC(codec: Any): codec is StrictC<Props> {
   return (codec as any)._tag === 'StrictType'


Test patch:
diff --git a/test/record.ts b/test/record.ts
index e982cc3..67d0001 100644
--- a/test/record.ts
+++ b/test/record.ts
@@ -10,172 +10,264 @@ import {
 } from './helpers'
 
 describe('record', () => {
-  describe('name', () => {
-    it('should assign a default name', () => {
-      const T = t.record(t.string, t.number)
-      assert.strictEqual(T.name, '{ [K in string]: number }')
-    })
+  describe('nonEnumerableRecord', () => {
+    describe('name', () => {
+      it('should assign a default name', () => {
+        const T = t.record(t.string, t.number)
+        assert.strictEqual(T.name, '{ [K in string]: number }')
+      })
 
-    it('should accept a name', () => {
-      const T = t.record(t.string, t.number, 'T')
-      assert.strictEqual(T.name, 'T')
+      it('should accept a name', () => {
+        const T = t.record(t.string, t.number, 'T')
+        assert.strictEqual(T.name, 'T')
+      })
     })
-  })
 
-  describe('is', () => {
-    it('should return `true` on valid inputs', () => {
-      const T1 = t.record(t.string, t.number)
-      assert.strictEqual(T1.is({}), true)
-      assert.strictEqual(T1.is({ a: 1 }), true)
+    describe('is', () => {
+      it('should return `true` on valid inputs', () => {
+        const T1 = t.record(t.string, t.number)
+        assert.strictEqual(T1.is({}), true)
+        assert.strictEqual(T1.is({ a: 1 }), true)
 
-      const T2 = t.record(t.string, NumberFromString)
-      assert.strictEqual(T2.is({}), true)
-      assert.strictEqual(T2.is({ a: 1 }), true)
+        const T2 = t.record(t.string, NumberFromString)
+        assert.strictEqual(T2.is({}), true)
+        assert.strictEqual(T2.is({ a: 1 }), true)
 
-      const T3 = t.record(HyphenatedString, t.number)
-      assert.strictEqual(T3.is({}), true)
-      assert.strictEqual(T3.is({ 'a-a': 1 }), true)
-    })
+        const T3 = t.record(HyphenatedString, t.number)
+        assert.strictEqual(T3.is({}), true)
+        assert.strictEqual(T3.is({ 'a-a': 1 }), true)
+      })
 
-    it('should return `false` on invalid inputs', () => {
-      const T1 = t.record(t.string, t.number)
-      assert.strictEqual(T1.is({ a: 'a' }), false)
-      assert.strictEqual(T1.is(null), false)
-      assert.strictEqual(T1.is(new Number()), false)
-      // #407
-      assert.strictEqual(T1.is([]), false)
-
-      const T2 = t.record(t.string, NumberFromString)
-      assert.strictEqual(T2.is({ a: 'a' }), false)
-      assert.strictEqual(T2.is(null), false)
-      // #407
-      assert.strictEqual(T2.is([]), false)
-
-      const T3 = t.record(HyphenatedString, t.number)
-      assert.strictEqual(T3.is({ aa: 1 }), false)
-    })
+      it('should return `false` on invalid inputs', () => {
+        const T1 = t.record(t.string, t.number)
+        assert.strictEqual(T1.is({ a: 'a' }), false)
+        assert.strictEqual(T1.is(null), false)
+        assert.strictEqual(T1.is(new Number()), false)
+        // #407
+        assert.strictEqual(T1.is([]), false)
 
-    it('should not accept an array if the codomain is `unknown`', () => {
-      const T = t.record(t.string, t.unknown)
-      assert.strictEqual(T.is([]), false)
-    })
+        const T2 = t.record(t.string, NumberFromString)
+        assert.strictEqual(T2.is({ a: 'a' }), false)
+        assert.strictEqual(T2.is(null), false)
+        // #407
+        assert.strictEqual(T2.is([]), false)
 
-    it('should accept an array if the codomain is `any`', () => {
-      // tslint:disable-next-line: deprecation
-      const T = t.record(t.string, t.any)
-      assert.strictEqual(T.is([]), true)
-    })
-  })
+        const T3 = t.record(HyphenatedString, t.number)
+        assert.strictEqual(T3.is({ aa: 1 }), false)
+      })
 
-  describe('decode', () => {
-    it('should decode a isomorphic value', () => {
-      const T = t.record(t.string, t.number)
-      assertSuccess(T.decode({}))
-      assertSuccess(T.decode({ a: 1 }))
-    })
+      it('should not accept an array if the codomain is `unknown`', () => {
+        const T = t.record(t.string, t.unknown)
+        assert.strictEqual(T.is([]), false)
+      })
 
-    it('should return the same reference while decoding isomorphic values', () => {
-      const T = t.record(t.string, t.number)
-      const value1 = { a: 1 }
-      assertStrictSuccess(T.decode(value1), value1)
+      it('should accept an array if the codomain is `any`', () => {
+        // tslint:disable-next-line: deprecation
+        const T = t.record(t.string, t.any)
+        assert.strictEqual(T.is([]), true)
+      })
     })
 
-    it('should decode a prismatic value', () => {
-      const T = t.record(t.string, NumberFromString)
-      assertSuccess(T.decode({ a: '1' }), { a: 1 })
-    })
+    describe('decode', () => {
+      it('should decode a isomorphic value', () => {
+        const T = t.record(t.string, t.number)
+        assertSuccess(T.decode({}))
+        assertSuccess(T.decode({ a: 1 }))
+      })
 
-    it('should decode a prismatic key', () => {
-      const T = t.record(HyphenatedString, t.number)
-      assertSuccess(T.decode({ ab: 1 }), { 'a-b': 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 not decode an array if the codomain is `unknown`', () => {
-      const T = t.record(t.string, t.unknown)
-      assertFailure(T, [1], ['Invalid value [1] supplied to : { [K in string]: unknown }'])
-    })
+      it('should return the same reference while decoding isomorphic values', () => {
+        const T = t.record(t.string, t.number)
+        const value1 = { a: 1 }
+        assertStrictSuccess(T.decode(value1), value1)
+      })
+
+      it('should decode a prismatic value', () => {
+        const T = t.record(t.string, NumberFromString)
+        assertSuccess(T.decode({ a: '1' }), { a: 1 })
+      })
+
+      it('should decode a prismatic key', () => {
+        const T = t.record(HyphenatedString, t.number)
+        assertSuccess(T.decode({ ab: 1 }), { 'a-b': 1 })
+      })
+
+      it('should not decode an array if the codomain is `unknown`', () => {
+        const T = t.record(t.string, t.unknown)
+        assertFailure(T, [1], ['Invalid value [1] supplied to : { [K in string]: unknown }'])
+      })
+
+      it('should decode an array if the codomain is `any`', () => {
+        // tslint:disable-next-line: deprecation
+        const T = t.record(t.string, t.any)
+        assertSuccess(T.decode([1]))
+      })
 
-    it('should decode an array if the codomain is `any`', () => {
-      // tslint:disable-next-line: deprecation
-      const T = t.record(t.string, t.any)
-      assertSuccess(T.decode([1]))
+      it('should fail decoding an invalid value', () => {
+        const T1 = t.record(t.string, t.number)
+        assertFailure(T1, 1, ['Invalid value 1 supplied to : { [K in string]: number }'])
+        assertFailure(T1, { aa: 's' }, ['Invalid value "s" supplied to : { [K in string]: number }/aa: number'])
+        assertFailure(T1, new Number(), ['Invalid value 0 supplied to : { [K in string]: number }'])
+        // #407
+        assertFailure(T1, [], ['Invalid value [] supplied to : { [K in string]: number }'])
+        // #407
+        assertFailure(T1, [1], ['Invalid value [1] supplied to : { [K in string]: number }'])
+        const T2 = t.record(HyphenatedString, t.number)
+        assertFailure(T2, { a: 1 }, [
+          'Invalid value "a" supplied to : { [K in HyphenatedString]: number }/a: HyphenatedString'
+        ])
+      })
     })
 
-    it('should fail decoding an invalid value', () => {
-      const T = t.record(t.string, t.number)
-      assertFailure(T, 1, ['Invalid value 1 supplied to : { [K in string]: number }'])
-      assertFailure(T, { aa: 's' }, ['Invalid value "s" supplied to : { [K in string]: number }/aa: number'])
-      assertFailure(T, new Number(), ['Invalid value 0 supplied to : { [K in string]: number }'])
-      // #407
-      assertFailure(T, [], ['Invalid value [] supplied to : { [K in string]: number }'])
-      // #407
-      assertFailure(T, [1], ['Invalid value [1] supplied to : { [K in string]: number }'])
+    describe('encode', () => {
+      it('should encode a isomorphic value', () => {
+        const T = t.record(t.string, t.number)
+        assert.deepStrictEqual(T.encode({ a: 1 }), { a: 1 })
+      })
+
+      it('should return the same reference while decoding a isomorphic value', () => {
+        const T = t.record(t.string, t.number)
+        const a = { a: 1 }
+        assert.strictEqual(T.encode(a), a)
+      })
+
+      it('should encode a prismatic value', () => {
+        const T = t.record(t.string, NumberFromString)
+        assert.deepStrictEqual(T.encode({ a: 1 }), { a: '1' })
+      })
+
+      it('should encode a prismatic key', () => {
+        const T = t.record(HyphenatedString, t.number)
+        assert.deepStrictEqual(T.encode({ 'a-b': 1 }), { ab: 1 })
+      })
+
+      it('should accept an array if the codomain is `any`', () => {
+        // tslint:disable-next-line: deprecation
+        const T = t.record(t.string, t.any)
+        const a = [1]
+        assert.strictEqual(T.encode(a), a)
+      })
     })
 
-    it('should support literals as domain type', () => {
-      const T = t.record(t.literal('foo'), t.string)
-      assertSuccess(T.decode({ foo: 'bar' }))
-      assertFailure(T, { foo: 'bar', baz: 'bob' }, [
-        'Invalid value "baz" supplied to : { [K in "foo"]: string }/baz: "foo"'
-      ])
+    it('should return the same reference if validation succeeded if nothing changed', () => {
+      const T1 = t.record(t.string, t.number)
+      const value1 = { aa: 1 }
+      assertStrictEqual(T1.decode(value1), value1)
+      const T2 = t.record(
+        // tslint:disable-next-line: deprecation
+        t.refinement(t.string, s => s.length >= 2),
+        t.number
+      )
+      const value2 = { aa: 1 }
+      assertStrictEqual(T2.decode(value2), value2)
     })
 
-    it('should support keyof as domain type', () => {
-      const T = t.record(t.keyof({ foo: true, bar: true }), t.string)
-      assertSuccess(T.decode({ foo: 'bar' }))
-      assertFailure(T, { foo: 'bar', baz: 'bob' }, [
-        'Invalid value "baz" supplied to : { [K in "foo" | "bar"]: string }/baz: "foo" | "bar"'
-      ])
+    it('should return the same reference while encoding', () => {
+      const T1 = t.record(t.string, t.number)
+      assert.strictEqual(T1.encode, t.identity)
+      const T2 = t.record(HyphenatedString, t.number)
+      assert.strictEqual(T2.encode === t.identity, false)
     })
   })
 
-  describe('encode', () => {
-    it('should encode a isomorphic value', () => {
-      const T = t.record(t.string, t.number)
-      assert.deepStrictEqual(T.encode({ a: 1 }), { a: 1 })
-    })
+  describe('enumerableRecord', () => {
+    describe('getDomainKeys', () => {
+      it('should handle literal', () => {
+        assert.deepStrictEqual(t.getDomainKeys(t.literal('a')), { a: null })
+        assert.deepStrictEqual(t.getDomainKeys(t.literal(1)), undefined)
+      })
 
-    it('should return the same reference while decoding a isomorphic value', () => {
-      const T = t.record(t.string, t.number)
-      const a = { a: 1 }
-      assert.strictEqual(T.encode(a), a)
+      it('should handle keyof', () => {
+        assert.deepStrictEqual(t.getDomainKeys(t.keyof({ a: 1, b: 2 })), { a: 1, b: 2 })
+      })
+
+      it('should handle union', () => {
+        assert.deepStrictEqual(t.getDomainKeys(t.union([t.literal('a'), t.literal('b')])), { a: null, b: null })
+        assert.deepStrictEqual(t.getDomainKeys(t.union([t.literal('a'), t.string])), undefined)
+      })
     })
 
-    it('should encode a prismatic value', () => {
-      const T = t.record(t.string, NumberFromString)
-      assert.deepStrictEqual(T.encode({ a: 1 }), { a: '1' })
+    describe('name', () => {
+      it('should assign a default name', () => {
+        const T = t.record(t.literal('a'), t.number)
+        assert.strictEqual(T.name, '{ [K in "a"]: number }')
+      })
+
+      it('should accept a name', () => {
+        const T = t.record(t.literal('a'), t.number, 'T')
+        assert.strictEqual(T.name, 'T')
+      })
     })
 
-    it('should encode a prismatic key', () => {
-      const T = t.record(HyphenatedString, t.number)
-      assert.deepStrictEqual(T.encode({ 'a-b': 1 }), { ab: 1 })
+    describe('is', () => {
+      it('should return `true` on valid inputs', () => {
+        const T = t.record(t.literal('a'), t.string)
+        assert.strictEqual(T.is({ a: 'a' }), true)
+        assert.strictEqual(T.is({ a: 'a', b: 1 }), true)
+      })
+
+      it('should return `false` on invalid inputs', () => {
+        const T = t.record(t.literal('a'), t.string)
+        assert.strictEqual(T.is({}), false)
+      })
     })
 
-    it('should accept an array if the codomain is `any`', () => {
-      // tslint:disable-next-line: deprecation
-      const T = t.record(t.string, t.any)
-      const a = [1]
-      assert.strictEqual(T.encode(a), a)
+    describe('decode', () => {
+      it('should support literals as domain type', () => {
+        const T = t.record(t.literal('a'), t.string)
+        assertSuccess(T.decode({ a: 'a' }), { a: 'a' })
+        assertSuccess(T.decode({ a: 'a', b: 1 }), { a: 'a' })
+        assertFailure(T, {}, ['Invalid value undefined supplied to : { [K in "a"]: string }/a: string'])
+        assertFailure(T, { a: 1 }, ['Invalid value 1 supplied to : { [K in "a"]: string }/a: string'])
+      })
+
+      it('should return the same reference while decoding isomorphic values', () => {
+        const T = t.record(t.literal('a'), t.string)
+        const value1 = { a: 'a' }
+        assertStrictSuccess(T.decode(value1), value1)
+      })
+
+      it('should support keyof as domain type', () => {
+        const T = t.record(t.keyof({ a: null, b: null }), t.string)
+        assertSuccess(T.decode({ a: 'a', b: 'b' }), { a: 'a', b: 'b' })
+        assertSuccess(T.decode({ a: 'a', b: 'b', c: 1 }), { a: 'a', b: 'b' })
+        assertFailure(T, {}, [
+          'Invalid value undefined supplied to : { [K in "a" | "b"]: string }/a: string',
+          'Invalid value undefined supplied to : { [K in "a" | "b"]: string }/b: string'
+        ])
+        assertFailure(T, { a: 'a' }, ['Invalid value undefined supplied to : { [K in "a" | "b"]: string }/b: string'])
+        assertFailure(T, { b: 'b' }, ['Invalid value undefined supplied to : { [K in "a" | "b"]: string }/a: string'])
+      })
+
+      it('#391', () => {
+        const T = t.intersection([t.record(t.literal('a'), t.string), t.record(t.string, t.unknown)])
+        assertSuccess(T.decode({ a: 'a', b: 'b' }), { a: 'a', b: 'b' })
+        assertFailure(T, { b: 'b' }, [
+          'Invalid value undefined supplied to : ({ [K in "a"]: string } & { [K in string]: unknown })/0: { [K in "a"]: string }/a: string'
+        ])
+      })
     })
-  })
 
-  it('should return the same reference if validation succeeded if nothing changed', () => {
-    const T1 = t.record(t.string, t.number)
-    const value1 = { aa: 1 }
-    assertStrictEqual(T1.decode(value1), value1)
-    const T2 = t.record(
-      // tslint:disable-next-line: deprecation
-      t.refinement(t.string, s => s.length >= 2),
-      t.number
-    )
-    const value2 = { aa: 1 }
-    assertStrictEqual(T2.decode(value2), value2)
-  })
+    describe('encode', () => {
+      it('should encode a isomorphic value', () => {
+        const T = t.record(t.literal('a'), t.number)
+        assert.deepStrictEqual(T.encode({ a: 1 }), { a: 1 })
+      })
 
-  it('should return the same reference while encoding', () => {
-    const T1 = t.record(t.string, t.number)
-    assert.strictEqual(T1.encode, t.identity)
-    const T2 = t.record(HyphenatedString, t.number)
-    assert.strictEqual(T2.encode === t.identity, false)
+      it('should return the same reference while decoding a isomorphic value', () => {
+        const T = t.record(t.literal('a'), t.number)
+        const a = { a: 1 }
+        assert.strictEqual(T.encode(a), a)
+      })
+
+      it('should encode a prismatic value', () => {
+        const T = t.record(t.literal('a'), NumberFromString)
+        assert.deepStrictEqual(T.encode({ a: 1 }), { a: '1' })
+      })
+    })
   })
 })