1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-25

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>

複数のオブジェクトのうち、どちらかにのみ存在するプロパティの存在を 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

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

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

TS play ground

.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として使えるようにはしてません。
(もちろん、ユーザ定義型ガード関数を作って自前で実装することはできます)

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

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

個人的には、上記issueにて投稿されているDaniel Rosenwasser氏の懸念コメントに、なるほどと思いました。

途中でも記載しましたが、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の方がむしろ正確なプロパティチェックをしているといえます。

もちろん、そんなケースどれだけあるよ?という感じもしますが😅
(懸念を示した際のコメントにもmaybe that's too pedantic.と言ってます)

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

まとめ

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

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

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?