TypeScript: interfaceにはinstanceofが使えないので、ユーザ定義タイプガードで対応する

TypeScriptには、コンパイラに型情報を提供することと、実行時の型チェックを同時に達成できる[Type Guard](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types)という仕組みがある。

## instanceofタイプガード

[instanceofタイプガード](https://www.typescriptlang.org/docs/handbook/advanced-types.html#instanceof-type-guards)を使うと、オブジェクトのクラスによって別の処理を行うことができる。例えば、次のコードはコンパイルも通るし、JSコードも問題なく実行される。

```ts
class Foo {
  foo!: number
}

class Bar {
  bar!: number
}

function getNumber(arg: Foo | Bar): number {
  if (arg instanceof Foo) {
    return arg.foo
  } else {
    return arg.bar
  }
}
```

JavaScriptへのコンパイル結果[^target:esnext]は次のようになる:

[^target:esnext]: `tsc`の`target`オプションが`esnext`の場合。

```js
"use strict";
class Foo {
}
class Bar {
}
function getNumber(arg) {
    if (arg instanceof Foo) {
        return arg.foo;
    }
    else {
        return arg.bar;
    }
}
```

## interfaceにはinstanceofタイプガードが使えない

一方、`interface`はというと、instanceofタイプガードが使えない。次のコードはコンパイルエラーになる:

```ts
interface Foo {
  foo: number
}

interface Bar {
  bar: number
}

function getNumber(arg: Foo | Bar): number {
  if (arg instanceof Foo) {
    return arg.foo
  } else {
    return arg.bar
  }
}
```

コンパイル結果:

```ts
not-works.ts:10:22 - error TS2693: 'Foo' only refers to a type, but is being used as a value here.

10   if (arg instanceof Foo) {
                        ~~~

not-works.ts:11:16 - error TS2339: Property 'foo' does not exist on type 'Foo | Bar'.
  Property 'foo' does not exist on type 'Bar'.

11     return arg.foo
                  ~~~

not-works.ts:13:16 - error TS2339: Property 'bar' does not exist on type 'Foo | Bar'.
  Property 'bar' does not exist on type 'Foo'.

13     return arg.bar
                  ~~~


Found 3 errors.
```

## なぜinstanceofが使えないか？

classのサンプルコードとinterfaceのサンプルコードは、見た目がほとんど同じだが、どうしてinterfaceだけコンパイルエラーになるのか？

理由は、`interface`はTypeScript固有の構文で、コンパイル時にだけ使用され、JavaScriptにコンパイルされるときにはinterfaceの情報は消去され、コンパイル結果のコードには残らないためである。

下記TSコードのコンパイル結果は、

```ts
interface Foo {
  foo: number
}

interface Bar {
  bar: number
}

const foo: Foo = {foo: 1}
const bar: Bar = {bar: 2}
```

次のようなJSになる:

```js
"use strict";
const foo = { foo: 1 };
const bar = { bar: 2 };
```

`interface Foo`と`interface Bar`がごっそり無くなっているのが分かる。


また、[`instanceof`演算子は、`object instanceof constructor`という構文である](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/instanceof#%E6%A7%8B%E6%96%87)ため、第二オペランドにconstructorしか取れないため、そもそも`interface`を渡すのも正しくない。


## interfaceのタイプガードはどうやったらいいか？

では、interfaceのタイプガードを実現するにはどうしたらいいか？

TypeScriptには[ユーザ定義タイプガード](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)という仕組みがあり、これを利用する。

先述のコンパイルできなかった`instanceof`タイプガードを使ったTypeScriptコードをユーザ定義タイプガードに書き換えると次のようになる:

```ts
interface Foo {
  foo: number
}

interface Bar {
  bar: number
}

// ユーザ定義タイプガード
function implementsFoo(arg: any): arg is Foo {
  return arg !== null &&
    typeof arg === "object" &&
    typeof arg.foo === "number"
}

function getNumber(arg: Foo | Bar): number {
  if (implementsFoo(arg)) { // implementsFooをタイプガードに使用
    return arg.foo
  } else {
    return arg.bar
  }
}
```

注目する点としては、`implementsFoo`という関数を定義して、これをタイプガードに使っているところだ。

この関数がユーザ定義タイプガードで、戻り値は`引数名 is 型`という形にする。

```ts
function implementsFoo(arg: any): arg is Foo {
  return arg !== null &&
    typeof arg === "object" &&
    typeof arg.foo === "number"
}
```

`arg`は`any`型で宣言されており、`object`以外にも`null`や`undefined`が与えられることも想定しなければならない。そのため、この関数の型チェックは3つの式からなる。

1. `arg !== null`: これは`arg`が`null`でないことをチェックしている。JavaScriptでは`typeof null === "object"`が成り立つため、2つ目のチェックでは不十分。
2. `typeof arg === "object"`: `undefined`でないことをチェックするのが目的。このチェックがないと、次の`arg.foo`の実行時チェックで「TypeError: Cannot read property 'foo' of undefined」が発生してしまう。
3. `typeof arg.foo === "number"`: 最後のこれは、`foo`が数値型であるかチェックする。`foo`が文字列など予期しない型の場合を考慮している。

この`implementsFoo`関数はコンパイル結果のJavaScriptコードにも残るため、コンパイル時の型チェックだけでなく、実行時の型チェックとしても機能してくれる:


```js
"use strict";
function implementsFoo(arg) {
    return arg !== null &&
        typeof arg === "object" &&
        typeof arg.foo === "number";
}
function getNumber(arg) {
    if (implementsFoo(arg)) {
        return arg.foo;
    }
    else {
        return arg.bar;
    }
}
```

---

最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:→[Twitter@suin](https://twitter.com/suin)