Repository: gcanti/io-ts (ts)

Issue:
Keys expecting values t.any can be omitted
Hello,

I just started to draft some property based tests using [fast-check](https://github.com/dubzzz/fast-check/) over [io-ts](https://github.com/gcanti/io-ts) and discovered what seems to be a bug. The property based tests I have implemented so far are available [here](https://github.com/dubzzz/fast-check-examples/blob/master/test/packages/io-ts/test.js).

Basically it seems that if a type has a key whose value is `t.any`, then this key can be omitted.

```js
const M = t.type({
  key: t.any
});
const obj = {};
M.decode(obj);
// right({})
// I was expecting to have a left(...) in this case
```

Do you confirm the issue?

Pull request description:
Bug Fix: should emit expected keys while decoding, fix #214
/cc @dubzzz @corhere 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 955124d..55c70d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,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.
 
+# 1.3.3
+
+- **Bug Fix**
+  - align `TaggedExact` definition with siblings, fix #223 (@gcanti)
+
 # 1.3.2
 
 - **Bug Fix**
diff --git a/package.json b/package.json
index e40f053..ebd8e23 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "io-ts",
-  "version": "1.3.2",
+  "version": "1.3.3",
   "description": "TypeScript compatible runtime type system for IO validation",
   "files": [
     "lib"
diff --git a/src/index.ts b/src/index.ts
index 873a1ef..0d8b4d6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -353,11 +353,13 @@ export class KeyofType<D extends { [key: string]: mixed }> extends Type<keyof D>
   }
 }
 
+const hasOwnProperty = Object.prototype.hasOwnProperty
+
 export const keyof = <D extends { [key: string]: mixed }>(
   keys: D,
   name: string = `(keyof ${JSON.stringify(Object.keys(keys))})`
 ): KeyofType<D> => {
-  const is = (m: mixed): m is keyof D => string.is(m) && keys.hasOwnProperty(m)
+  const is = (m: mixed): m is keyof D => string.is(m) && hasOwnProperty.call(keys, m)
   return new KeyofType(name, is, (m, c) => (is(m) ? success(m) : failure(m, c)), identity, keys)
 }
 
@@ -515,7 +517,8 @@ export const type = <P extends Props>(
         return false
       }
       for (let i = 0; i < len; i++) {
-        if (!types[i].is(m[keys[i]])) {
+        const k = keys[i]
+        if (!hasOwnProperty.call(m, k) || !types[i].is(m[k])) {
           return false
         }
       }
@@ -531,19 +534,25 @@ export const type = <P extends Props>(
         const errors: Errors = []
         for (let i = 0; i < len; i++) {
           const k = keys[i]
-          const ok = o[k]
+          if (!hasOwnProperty.call(a, k)) {
+            if (a === o) {
+              a = { ...o }
+            }
+            a[k] = a[k]
+          }
+          const ak = a[k]
           const type = types[i]
-          const validation = type.validate(ok, appendContext(c, k, type))
+          const validation = type.validate(ak, appendContext(c, k, type))
           if (validation.isLeft()) {
             pushAll(errors, validation.value)
           } else {
-            const vok = validation.value
-            if (vok !== ok) {
+            const vak = validation.value
+            if (vak !== ak) {
               /* istanbul ignore next */
               if (a === o) {
                 a = { ...o }
               }
-              a[k] = vok
+              a[k] = vak
             }
           }
         }
@@ -599,15 +608,53 @@ export const partial = <P extends Props>(
   for (let i = 0; i < len; i++) {
     partials[keys[i]] = union([types[i], undefinedType])
   }
-  const partial = type(partials)
   return new PartialType(
     name,
-    partial.is as any,
-    partial.validate as any,
+    (m): m is TypeOfPartialProps<P> => {
+      if (!Dictionary.is(m)) {
+        return false
+      }
+      for (let i = 0; i < len; i++) {
+        const k = keys[i]
+        if (!partials[k].is(m[k])) {
+          return false
+        }
+      }
+      return true
+    },
+    (m, c) => {
+      const dictionaryValidation = Dictionary.validate(m, c)
+      if (dictionaryValidation.isLeft()) {
+        return dictionaryValidation
+      } else {
+        const o = dictionaryValidation.value
+        let a = o
+        const errors: Errors = []
+        for (let i = 0; i < len; i++) {
+          const k = keys[i]
+          const ak = a[k]
+          const type = partials[k]
+          const validation = type.validate(ak, appendContext(c, k, type))
+          if (validation.isLeft()) {
+            pushAll(errors, validation.value)
+          } else {
+            const vak = validation.value
+            if (vak !== ak) {
+              /* istanbul ignore next */
+              if (a === o) {
+                a = { ...o }
+              }
+              a[k] = vak
+            }
+          }
+        }
+        return errors.length ? failures(errors) : success(a as any)
+      }
+    },
     useIdentity(types, len)
       ? identity
       : a => {
-          const s: { [key: string]: any } = { ...(a as any) }
+          const s: { [key: string]: any } = { ...a }
           for (let i = 0; i < len; i++) {
             const k = keys[i]
             const ak = a[k]
@@ -1058,20 +1105,20 @@ export type TaggedIntersectionArgument<Tag extends string> =
   | [Mixed, Mixed, Mixed, Mixed, Tagged<Tag>]
 export interface TaggedIntersection<Tag extends string, A, O = A>
   extends IntersectionType<TaggedIntersectionArgument<Tag>, A, O> {}
-export interface TaggedExact<Tag extends string> extends ExactType<Tagged<Tag>> {}
+export interface TaggedExact<Tag extends string, A, O = A> extends ExactType<Tagged<Tag>, A, O> {}
 export type Tagged<Tag extends string, A = any, O = A> =
   | InterfaceType<TaggedProps<Tag>, A, O>
   | StrictType<TaggedProps<Tag>, A, O>
   | TaggedRefinement<Tag, A, O>
   | TaggedUnion<Tag, A, O>
   | TaggedIntersection<Tag, A, O>
-  | TaggedExact<Tag>
+  | TaggedExact<Tag, A, O>
   | RecursiveType<any, A, O>
 
 export const isTagged = <Tag extends string>(tag: Tag): ((type: Mixed) => type is Tagged<Tag>) => {
   const f = (type: Mixed): type is Tagged<Tag> => {
     if (type instanceof InterfaceType || type instanceof StrictType) {
-      return type.props.hasOwnProperty(tag)
+      return hasOwnProperty.call(type.props, tag)
     } else if (type instanceof IntersectionType) {
       return type.types.some(f)
     } else if (type instanceof UnionType) {
@@ -1153,7 +1200,7 @@ export const taggedUnion = <Tag extends string, RTS extends Array<Tagged<Tag>>>(
     hash[String(value)] = i
   }
   const isTagValue = useHash
-    ? (m: mixed): m is string | number | boolean => string.is(m) && hash.hasOwnProperty(m)
+    ? (m: mixed): m is string | number | boolean => string.is(m) && hasOwnProperty.call(hash, m)
     : (m: mixed): m is string | number | boolean => values.indexOf(m as any) !== -1
   const getIndex: (tag: string | number | boolean) => number = useHash
     ? tag => hash[tag as any]
@@ -1252,7 +1299,7 @@ export function exact<RT extends HasProps>(
   const props: Props = getProps(type)
   return new ExactType(
     name,
-    (m): m is TypeOf<RT> => type.is(m) && Object.getOwnPropertyNames(m).every(k => props.hasOwnProperty(k)),
+    (m): m is TypeOf<RT> => type.is(m) && Object.getOwnPropertyNames(m).every(k => hasOwnProperty.call(props, k)),
     (m, c) => {
       const looseValidation = type.validate(m, c)
       if (looseValidation.isLeft()) {
@@ -1264,7 +1311,7 @@ export function exact<RT extends HasProps>(
         const errors: Errors = []
         for (let i = 0; i < len; i++) {
           const key = keys[i]
-          if (!props.hasOwnProperty(key)) {
+          if (!hasOwnProperty.call(props, key)) {
             errors.push(getValidationError(o[key], appendContext(c, key, never)))
           }
         }


Test patch:
diff --git a/test/interface.ts b/test/interface.ts
index 2ce759b..e9a44b4 100644
--- a/test/interface.ts
+++ b/test/interface.ts
@@ -8,6 +8,22 @@ describe('interface', () => {
     assertSuccess(T.decode({ a: 's' }))
   })
 
+  it('should emit expected keys while decoding (#214)', () => {
+    const T1 = t.type({ a: t.any })
+    assert.deepEqual(T1.decode({}).value, { a: undefined })
+    assert.deepEqual(T1.decode(Object.create(null)).value, { a: undefined })
+
+    const T2 = t.type({ a: t.union([t.number, t.undefined]) })
+    const input = {}
+    assert.deepEqual(T2.decode(input).value, { a: undefined })
+    assert.deepEqual(input, {})
+
+    const jsonTurnaround = <A>(type: t.Type<A>, a: A): t.Validation<A> => {
+      return type.decode(JSON.parse(JSON.stringify(type.encode(a))))
+    }
+    assert.deepEqual(jsonTurnaround(T2, { a: undefined }).value, { a: undefined })
+  })
+
   it('should keep unknown properties', () => {
     const T = t.interface({ a: t.string })
     const validation = T.decode({ a: 's', b: 1 })
@@ -55,10 +71,17 @@ describe('interface', () => {
     const T1 = t.type({ a: t.number })
     assert.strictEqual(T1.is({ a: 0 }), true)
     assert.strictEqual(T1.is(undefined), false)
+
     const T2 = t.type({ a: DateFromNumber })
     assert.strictEqual(T2.is({ a: new Date(0) }), true)
     assert.strictEqual(T2.is({ a: 0 }), false)
     assert.strictEqual(T2.is(undefined), false)
+
+    const T3 = t.type({ a: t.union([t.number, t.undefined]) })
+    assert.strictEqual(T3.is({ a: 1 }), true)
+    assert.strictEqual(T3.is({ a: undefined }), true)
+    assert.strictEqual(T3.is({}), false)
+    assert.strictEqual(T3.is(Object.create(null)), false)
   })
 
   it('should preserve additional properties while encoding', () => {
diff --git a/test/partial.ts b/test/partial.ts
index 403af13..33b775d 100644
--- a/test/partial.ts
+++ b/test/partial.ts
@@ -18,18 +18,13 @@ describe('partial', () => {
 
   it('should not add optional keys', () => {
     const T = t.partial({ a: t.number })
-    assert.strictEqual(
-      T.decode({})
-        .fold<any>(t.identity, t.identity)
-        .hasOwnProperty('a'),
-      false
-    )
-    assert.strictEqual(
-      T.decode({ b: 1 })
-        .fold<any>(t.identity, t.identity)
-        .hasOwnProperty('a'),
-      false
-    )
+    const input1 = {}
+    assertStrictEqual(T.decode(input1), input1)
+    const input2 = { a: undefined }
+    assertStrictEqual(T.decode(input2), input2)
+    assert.deepEqual(T.decode({ b: 1 }).value, { b: 1 })
+    const input3 = { a: undefined, b: 1 }
+    assertStrictEqual(T.decode(input3), input3)
   })
 
   it('should return the same reference if validation succeeded', () => {
@@ -40,6 +35,7 @@ describe('partial', () => {
 
   it('should fail validating an invalid value', () => {
     const T = t.partial({ a: t.number })
+    assertFailure(T.decode(null), ['Invalid value null supplied to : PartialType<{ a: number }>'])
     assertFailure(T.decode({ a: 's' }), [
       'Invalid value "s" supplied to : PartialType<{ a: number }>/a: (number | undefined)/0: number',
       'Invalid value "s" supplied to : PartialType<{ a: number }>/a: (number | undefined)/1: undefined'