なぜ(今のところ)Type ScriptはObject.hasOwn()をType Guardとして扱わないのか調べてみた


### in はtype guard

複数のオブジェクト型を引数にとり、型に応じて処理を行う時には、type guard処理を書くことが多いですね。
typescript, nuxtだと以下のような感じです。

```.vue
<template>
  {{ showDetail() }}
</template>
<script lang="ts" setup>
type User = {
  name: string
  email: string
  companyID: string
}
type AdminUser = {
  name: string
  email: string
  role: string
}
const props = defineProps<{
  data: AdminUser | User
}>()

const showDetail = () => {
  if ('companyID' in props.data) {
    return `User: ${props.data.name}/${props.data.companyID}`
  } else {
    return `AdminUser: ${props.data.name}/${props.data.role}`
  }
}
</script>
```

https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing

複数のオブジェクトのうち、どちらかにのみ存在するプロパティの存在を in で判定することで、TSの型解釈が働き、エディタ上のプロパティ参照のエラーもなくなります。


#### in を使う時の注意

in を使った場合、**プロトタイプオブジェクトから継承したプロパティ**を含めて存在するかの判定がなされます。

なので、開発により定義してないプロパティもヒットする可能性が、あるにはあります。

### なぜ Object.hasOwn(), Object.hasOwnProperty() では、プロトタイプオブジェクトから継承したプロパティを除外する仕様になっています。

＊`hasOwnProperty()`についてはプロパティを上書きすることで動作が保証できなくなってしまう懸念があり、`Object.hasOwn()`が推奨されています。

```.ts
const object1 = {
  a: 'somestring',
  b: 42,
}
const objectForExplainHasOwnPropertyIsDeprecate = {
  a: 'somestring',
  b: 42,
  hasOwnProperty: (val: string) => false
}

if ('toString' in object1) {
  console.log('in toString exist!')
}


if (object1.hasOwnProperty('toString')) {
  console.log('toString exist with hasOwnProperty!')
}
if (object1.hasOwnProperty('a')) {
  console.log('toString exist with a!')
}
if (objectForExplainHasOwnPropertyIsDeprecate.hasOwnProperty('a')) {
  console.log('never exec because hasOwnProperty is overrided')
}

if (Object.hasOwn(object1, 'toString')) {
  console.log('toString exist !!!')
}
if (Object.hasOwn(object1, 'a')) {
  console.log('a exist !!!')
}
```

[TS Play ground](https://www.typescriptlang.org/ja/play/?target=9#code/MYewdgzgLgBCBGArApsKBGGBeGBvAsAFAwwCGAXDAOQQgC2y0ATgJZgDmVANESfJQBYATD0IBfIqEiwEKNADEQTAKIAPAA4AbUmwASpCAHkA7mAAKTEOuRMoATwCSEACLJ1TVKSjJseXmUoaekYoVg5uf34YYVESAAsDE3NLa1s7SgAKADdSTUpmNnYASmwAPhgAM1yIZCIJQiIWCpgMqigQAGVQwqoYNjgkVAwSgmIYKVpNZAA6TRB2Vv72rrD2GGRVFmgAQioiuqJDwiaW2SH0aYSjUwsrG3tW5e7wopH-CZAp2fnHzue1jZbWDGFhQOIwK5JW6pey7fbiRrNDJnNAXSE3FL3OytUh7N5jD5fOYLNp-VbrTbQGAgsFkOEHY5IlFQRQqDTaPSJDF3NJOVzuTzeS5c5I8h5UXGvPwE8CTGbE1pgZBZGwU1AweCeACuNQhIuhWL6EDgKqYrAAJshzXsGYiWoZBmhhdcwMjHRguNQnqs8dKSIT5T9SStChSgTBtpGbQjGfb3c6km65B7qJL8f7ZZ9AyTSGGqZH6eIgA)

### `Object.hasOwn()` はtype guardが効かない

以下のコードのようなコードを書いた場合、`Object.hasOwn()`を使った方はTSのエラーが表示されます。

[TS play ground](https://www.typescriptlang.org/play/?target=9#code/C4TwDgpgBAqgzhATlAvFA3gWAFBSgOwEMBbCALijmEQEt8BzHPCYwmgGwqtoaagGMA9sTCF8IAJIARLtTqNsAXxyhIUAIIATYnXhJUGPkVKyeC5qw6n5fRIPblKc3kpw4h+KlACuCZGixcAhJHAHIACxo7UIAaPiERMUkZKFCARgzYnGVsDy84cMEAdykIYDZ2AwAKXyQKPWQAHw1tXT8ASlQAPkMgmgAzKCqAeQAjACsIfmAAOnDCOGGi-Bq-GNSE0XFpUPbOwLw8AHojqEBTuUBoOUA7BgAVAGUoJDtEQBkGQC83QHxXPmPTgAU7SCIUAbYRbZKhKCaQQQOAEQTAR4ADxoXkE+CgqmgoQaUGaWh0+AaoRm3ygJyg-0EgOBoU2SR2kOhsPw8KRKIRaIx4Cx+LaSGJVQATABmYUATnapMQZW8iHRAAMGhQACToWqIGbGCCKI6q9UzOnbKSKeV8RSPdgIXqHKDS4CyhW8wl+FVqvyakI6vXuuwOE1m7LuNH5QolMoVQXVdX1Py4loEhqdFA9A5k06AZQZAIYMgEiGQD2DIAKhkAlwyAH4YXoALCMAXJ6AVQZADEMgCsGQAiDHwBkNaaD6VIIXQfB1rYc7Q6oIqXVBvUgPaQvW7x4bkv6guaIJboKm8AO5UOnUrR9ONVqp-rfdrTQvskA)

```.ts
type User = {
  name: string
  email: string
  companyID: string
}
type AdminUser = {
  name: string
  email: string
  role: string
}

const user = {
  name: 'hiro',
  companyID: '111',
}
const showDetail = (user: User | AdminUser) => {
  if (Object.hasOwn(user, 'companyID')) {
    // 以下のTS errorが発生
    // Property 'companyID' does not exist on type 'User | AdminUser'.
    // Property 'companyID' does not exist on type 'AdminUser'.(2339)
    return `User: ${user.name}/${user.companyID}`
  } else {
    return `AdminUser: ${user.name}/${user.role}`
  }
}
const showDetail2 = (user: User | AdminUser) => {
  // こちらはTS errorが表示されない
  if ('companyID' in user) {
    return `User: ${user.name}/${user.companyID}`
  } else {
    return `AdminUser: ${user.name}/${user.role}`
  }
}

```

### なぜ Object.hasOwn() はtype guardとして認められてないのか

2024/12/25時点において、TSは`Object.hasOwn()`をtype guardとして使えるようにはしてません。
(もちろん、[ユーザ定義型ガード関数](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)を作って自前で実装することはできます)

実は、`Object.hasOwn()`をtype guardできるようにしようぜ！という議論も現在進行形でなされています。
・・・というか、2021

type scriptの公式リポジトリのissueを探るといくつも同じような話がされていますが、一番HOTなのは以下のissueです。


https://github.com/microsoft/TypeScript/issues/44253


個人的には、上記issueにて投稿されている[Daniel Rosenwasser](https://github.com/microsoft/TypeScript/issues/44253#issuecomment-848429256)氏の懸念コメントに、なるほどと思いました。

途中でも記載しましたが、`Object.hasOwn()`を使った場合はプロトタイプから継承したプロパティの存在チェックをしません。

でも、実際にオブジェクトの設定されている**変数はプロトタイプから継承したプロパティが存在するなら、アクセスすることができます。


ということは、以下のようなコードにおいて「`else`文に入ったけど、実は継承元のプロトタイプに`b`というプロパティがある」というケースもあるワケです。

```.ts
declare const abcd: { a: string, b: string } | { c: string, d: string };
if (Object.hasOwn(abcd, "b")) {
  abcd.b; // should be OK
} else {
  abcd.c // okay?
  abcd.d // okay?
}
```

このケースを考慮する場合、プロトタイプまで走査する`in`の方がむしろ正確なプロパティチェックをしているといえます。

もちろん、そんなケースどれだけあるよ？という感じもしますが😅
([懸念を示した際のコメント](https://github.com/microsoft/TypeScript/issues/44253#issuecomment-848429256)にも`maybe that's too pedantic.`と言ってます)

ただ、`in`,`Object.hasOwn()`のプロパティチェックの仕様が異なる以上、懸念をする方がいて議論が長引いてるのも納得です。

### まとめ

・`hasOwn()`派:開発において型の判定にプロトタイプのプロパティを含めたいわけないじゃん
・`in`派:プロトタイプからの継承元も含めて参照しうるkeyのプロパティ全てチェックしてこそ正確な型の判定ができるんじゃん

どちらの考え方も説得力がありますね。

現状でプロジェクト・チーム内で合意を取れるなら`Object.hasOwn()`を使ったtype guard関数を使って運用する、ということになりそうです。


今後のTSバージョンアップ次第では、いい感じに解消(統合?)されて、`in`を使ったtype guardから`hasOwn()`移行する作業が発生するかもしれません。