in はtype guard
複数のオブジェクト型を引数にとり、型に応じて処理を行う時には、type guard処理を書くことが多いですね。
typescript, nuxtだと以下のような感じです。
<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()
が推奨されています。
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 !!!')
}
Object.hasOwn()
はtype guardが効かない
以下のコードのようなコードを書いた場合、Object.hasOwn()
を使った方は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
というプロパティがある」というケースもあるワケです。
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()
移行する作業が発生するかもしれません。