Repository: gcanti/io-ts (ts)

Issue:
Getters can satisfy io-ts static types while failing to decode at runtime
## 🐛 Bug report

Given an object validator:

```
type Person = {
  name: string
}

const person: t.Type<Person> = t.strict({
  name: t.string,
})
```

Instances of classes with a `name` attribute will pass runtime validation. But instances of a class with a `name` getter will fail runtime validation because io-ts will see an `undefined` name.

Both of those uses pass the static type checks, even though the second one is rejected at runtime. The net effect is: the type system allows data that will definitely be rejected by io-ts at runtime.

### Reproducible example

This self-contained TypeScript file demonstrates the bug. Alice has a regular `name` property, type checks against `t.TypeOf<typeof person>`, and validates. Bob has a `name` getter, also type checks against `t.TypeOf<typeof person>`, but fails validation at runtime.

```typescript
import * as t from "io-ts"

type Person = {
  name: string
}

class Alice {
  public name: string

  constructor() {
    this.name = 'Alice'
  }
}

class Bob {
  get name() { return 'Bob' }
}

const person: t.Type<Person> = t.strict({
  name: t.string,
})

// Alice has a normal `name` property; we assign her to the type extracted from
// the io-ts validator; and everything works.
const alice: t.TypeOf<typeof person> = new Alice()
console.log("Alice object:", alice)
console.log("Alice decode:", person.decode(alice))
console.log()

// Bob has a `name` getter; again we assign him to the type extracted from the
// io-ts validator; but it fails at runtime even though the types check.
const bob: t.TypeOf<typeof person> = new Bob()
console.log("Bob object:", bob)
console.log("Bob decode:", person.decode(bob))
```

Here's the output that I see:

```
Alice object: Alice { name: 'Alice' }
Alice decode: { _tag: 'Right', right: Alice { name: 'Alice' } }

Bob object: Bob {}
Bob decode: {
  _tag: 'Left',
  left: [ { value: undefined, context: [Array], message: undefined } ]
}
```

### Additional context

Searching issues for "getter" only gives one result that seems unrelated to this, so hopefully this isn't a duplicate!

### Your environment

Which versions of io-ts are affected by this issue? Did this work in previous versions of io-ts?

| Software   | Version(s) |
| ---------- | ---------- |
| io-ts      | 2.1.1           |
| fp-ts      | 2.5.1           |
| TypeScript | 3.8.2           |


Pull request:
remove useless hasOwnProperty calls, closes #423

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da6166a..4806bfc 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.2
+
+- **Polish**
+  - remove useless `hasOwnProperty` calls, closes #423 (@gcanti)
+
 # 2.1.1
 
 - **Bug Fix**
diff --git a/package.json b/package.json
index d7cac00..89b8b39 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "io-ts",
-  "version": "2.1.1",
+  "version": "2.1.2",
   "description": "TypeScript compatible runtime type system for IO validation",
   "files": [
     "lib",
diff --git a/src/index.ts b/src/index.ts
index d12a85a..2f17b9e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -798,7 +798,7 @@ export const type = <P extends Props>(props: P, name: string = getInterfaceTypeN
       if (UnknownRecord.is(u)) {
         for (let i = 0; i < len; i++) {
           const k = keys[i]
-          if (!hasOwnProperty.call(u, k) || !types[i].is(u[k])) {
+          if (!types[i].is(u[k])) {
             return false
           }
         }
@@ -812,12 +812,6 @@ export const type = <P extends Props>(props: P, name: string = getInterfaceTypeN
         const errors: Errors = []
         for (let i = 0; i < len; i++) {
           const k = keys[i]
-          if (!hasOwnProperty.call(a, k)) {
-            if (a === o) {
-              a = { ...o }
-            }
-            a[k] = a[k]
-          }
           const ak = a[k]
           const type = types[i]
           const result = type.validate(ak, appendContext(c, k, type, ak))
@@ -825,7 +819,7 @@ export const type = <P extends Props>(props: P, name: string = getInterfaceTypeN
             pushAll(errors, result.left)
           } else {
             const vak = result.right
-            if (vak !== ak) {
+            if (vak !== ak || (vak === undefined && !hasOwnProperty.call(a, k))) {
               /* istanbul ignore next */
               if (a === o) {
                 a = { ...o }


Test patch:
diff --git a/test/strict.ts b/test/strict.ts
index c869385..35a4cf7 100644
--- a/test/strict.ts
+++ b/test/strict.ts
@@ -29,6 +29,19 @@ describe('strict', () => {
       assert.strictEqual(T.is({ a: 1, b: 1 }), true)
       assert.strictEqual(T.is(undefined), false)
     })
+
+    it('#423', () => {
+      class A {
+        get a() {
+          return 'a'
+        }
+        get b() {
+          return 'b'
+        }
+      }
+      const T = t.strict({ a: t.string, b: t.string })
+      assert.strictEqual(T.is(new A()), true)
+    })
   })
 
   describe('decode', () => {
@@ -57,6 +70,19 @@ describe('strict', () => {
       const T = t.strict({ foo: t.string })
       assertSuccess(T.decode({ foo: 'foo', bar: 1, baz: true }), { foo: 'foo' })
     })
+
+    it('#423', () => {
+      class A {
+        get a() {
+          return 'a'
+        }
+        get b() {
+          return 'b'
+        }
+      }
+      const T = t.strict({ a: t.string, b: t.string })
+      assertSuccess(T.decode(new A()))
+    })
   })
 
   describe('encode', () => {
diff --git a/test/type.ts b/test/type.ts
index f7914cd..3666391 100644
--- a/test/type.ts
+++ b/test/type.ts
@@ -35,6 +35,19 @@ describe('type', () => {
       const T = t.type({ a: t.string })
       assert.strictEqual(T.is({ a: 'a', b: 1 }), true)
     })
+
+    it('#423', () => {
+      class A {
+        get a() {
+          return 'a'
+        }
+        get b() {
+          return 'b'
+        }
+      }
+      const T = t.type({ a: t.string, b: t.string })
+      assert.strictEqual(T.is(new A()), true)
+    })
   })
 
   describe('decode', () => {
@@ -75,6 +88,19 @@ describe('type', () => {
       const T = t.interface({ a: t.string })
       assertSuccess(T.decode({ a: 'a' }))
     })
+
+    it('#423', () => {
+      class A {
+        get a() {
+          return 'a'
+        }
+        get b() {
+          return 'b'
+        }
+      }
+      const T = t.type({ a: t.string, b: t.string })
+      assertSuccess(T.decode(new A()))
+    })
   })
 
   describe('encode', () => {