LoginSignup
147
92

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-06-20

TypeScriptには、コンパイラに型情報を提供することと、実行時の型チェックを同時に達成できるType Guardという仕組みがある。

instanceofタイプガード

instanceofタイプガードを使うと、オブジェクトのクラスによって別の処理を行うことができる。例えば、次のコードはコンパイルも通るし、JSコードも問題なく実行される。

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へのコンパイル結果1は次のようになる:

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

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

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

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
  }
}

コンパイル結果:

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コードのコンパイル結果は、

interface Foo {
  foo: number
}

interface Bar {
  bar: number
}

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

次のようなJSになる:

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

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

また、instanceof演算子は、object instanceof constructorという構文であるため、第二オペランドにconstructorしか取れないため、そもそもinterfaceを渡すのも正しくない。

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

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

TypeScriptにはユーザ定義タイプガードという仕組みがあり、これを利用する。

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

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 型という形にする。

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

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

  1. arg !== null: これはargnullでないことをチェックしている。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コードにも残るため、コンパイル時の型チェックだけでなく、実行時の型チェックとしても機能してくれる:

"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


  1. tsctargetオプションがesnextの場合。 

147
92
3

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
147
92