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 Foo
とinterface 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"
}
arg
はany
型で宣言されており、object
以外にもnull
やundefined
が与えられることも想定しなければならない。そのため、この関数の型チェックは3つの式からなる。
-
arg !== null
: これはarg
がnull
でないことをチェックしている。JavaScriptではtypeof null === "object"
が成り立つため、2つ目のチェックでは不十分。 -
typeof arg === "object"
:undefined
でないことをチェックするのが目的。このチェックがないと、次のarg.foo
の実行時チェックで「TypeError: Cannot read property 'foo' of undefined」が発生してしまう。 -
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に書かない技術ネタなどもツイートしているので、よかったらフォローお願いします→Twitter@suin
-
tsc
のtarget
オプションがesnext
の場合。 ↩