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の場合。 ↩