TypeScript Property ‘xxxxx’ does not exist on type T のエラーに対して in を使わない方が良い理由

以下のコードがあります。

type T1 = {
    hoge: string
}

type T2 = {
    foo: number
}

const myFunc = (obj: T1 | T2): void => {
    console.log(obj.foo)
}
  • T1はhogeというプロパティのみを持つ
  • T2はfooというプロパティのみを持つ

このように定義したいわけです。(後述しますが、実際にはTypeScriptこのような解釈はしないことがミソです。「のみをもつ」というところが違います。)

このコードは Property 'foo’ does not exist on type 'T1 | T2’. でコンパイルエラーになります。

obj は T1 | T2 という型ですが、これは foo というプロパティが存在するかどうかについては確定していません。

T2 は foo プロパティが存在しますが、T1 には foo プロパティが存在しないというわけではありません

存在するかもしれないし、存在しないかもしれません。

つまり、TypeScriptとしては、T1型は冒頭に書いたように「T1はhogeというプロパティのみを持つ」とは解釈しないのです。

試しに以下のようなコードはコンパイルエラーになりません。

type T1 = {
    hoge: string
}

const o = {
    hoge: 'tami',
    foo: 123,
}

const t1Obj: T1 = o

これはTypeScriptが構造的部分型であるためです。

※余剰プロパティチェックを効かせた場合はコンパイルエラーになりますが、そこは割愛します。

コンパイルエラーを解消するために、in を使ってプロパティの存在をチェックする方法が一般的な回答になっていたりします。

const myFunc = (obj: T1 | T2): void => {
    // foo というプロパティがあればT2、そうでなければT1としたい
    if ('foo' in obj) {
        console.log('This is T2', obj.foo)
    }else{
        console.log('This is T1', obj.hoge)
    }
}

しかし、これには落とし穴があります。

先ほど記載したように、T1はfooプロパティを持っている可能性もあるためです。

type T1 = {
    hoge: string
}

type T2 = {
    foo: number
}

const myFunc = (obj: T1 | T2): void => {
    if ('foo' in obj) {
        console.log('This is T2', obj.foo)
    }else{
        console.log('This is T1',obj.hoge)
    }
}

const o = {
    hoge: 'tami',
    foo: 123,
}

const t1Obj: T1 = o // T1 には foo をもつオブジェクトも割り当てられてしまう

myFunc(t1Obj) // t1ObjはT1型のはずだが、コンソールには「This is T2」とでてしまう

t1Obj は T1型です。にもかかわらず、コードとしてはT2として処理されています。(想定外)

これを防止する場合は、型を変更します。

つまり、以下を明示的にTypeScriptに教えてあげます。

  • T1にはfooというプロパティが存在したとしてもundefinedである
  • T2にはhogeというプロパティが存在したとしてもundefinedである
type T1 = {
    hoge: string
    foo?: undefined
}

type T2 = {
    foo: number
    hoge?: undefined
}

const myFunc = (obj: T1 | T2): void => {
    if (obj.foo !== undefined) {
        console.log('This is T2', obj.foo)
    }else{
        console.log('This is T1',obj.hoge)
    }
}

const o = {
    hoge: 'tami',
    foo: 123,
}

const t1Obj: T1 = o // ここでコンパイルエラーになってくれる

myFunc(t1Obj)

このようにすることで、「const t1Obj: T1 = o」のところで、TypeScriptのコンパイルエラーになってくれるので、事前に気が付くことができます。