JavaScriptではtypeof, instanceof
というその変数の型が何であるかを判別できる演算子があります。主にtypeof
はプリミティブ型に、instanceof
はクラスに対して使います。
TypeScriptはこれらの演算子で型を判別したあと、そのif, case
ブロックではその変数はキャストをすることなくその型であるかのように振る舞うことができます。このTypeScriptの機能を型ガードと呼びます。以下はtypeof
の例です。typeof
をswitch
で使用し、その結果をcase
で分岐させています。
function toString(n: unknown): string {switch (typeof n) {case 'undefined':return 'undefined';case 'object':if (n === null) {return 'null';}return Object.prototype.toString.call(n);case 'number':return n.toExponential();case 'string':return n;default:return Object.prototype.toString.call(n);}}
ここで注目していただきたいのはcase 'number'
の箇所です。ここで変数n
はunknown
型からこのブロックに限りnumber
型としてみなされnumber
型のメソッドであるtoExponential()
が使えるようになっています。
変数がある型AまたはBであるようなとき、TypeScriptではユニオン型を使い表現できます。ユニオン型については専門のページがあるため詳しい説明はそちらに譲ります。ここでは型がAまたはBであるときはユニオン型を使うとA | B
と書くことができるということだけを覚えておいてください。
ユニオン型はそのどちらか(あるいはどれか)が不確定であることを示していますがtypeof
またはinstanceof
を使うことによってそのユニオン型からどの型であるかを確定させることができます。
function toEuropeanNumber(n: string | null): string {if (typeof n === 'string') {// n is stringconsole.log(n.split(',').join('.'));}// ...}
型が確定しているのはif
ブロックの中だけであり、そのブロックを抜けてしまうと変数は再び元の型として解釈されます。
function toEuropeanNumber(n: string | null): string {if (typeof n === 'string') {// n is stringconsole.log(n.split(',').join('.'));}// n is string | nullconsole.log(n.split(',').join('.'));// Object is possibly 'null'.}
上の例はif
ブロックを抜けた後でstring
型のメソッドであるsplit()
を使おうとしているためTypeScriptから指摘を受けています。
型ガードを使うためには毎回if
ブロックを用意しなければならないかというとそうではありません。つまり次のようにする必要はありません。
function toEuropeanNumber(n: string | null): string {if (typeof n === 'string') {// n is stringconsole.log(n.split(',').join('.'));return n.split(',').join('.');}if (n === null) {return 'NaN';}//return what?;}
このようなときはif
ブロックの中でreturn
が行われている、あるいはelse
ブロックが使われていればTypeScriptはそれ以外の場合を想定してくれます。
次の例のようにあるユニオン型からひとつの型を確定させ、確定させたif
ブロックでreturn
が行われるとそのブロックを抜けたあとはユニオン型からその型を抜いたとしてTypeScriptは自動的に解釈してくれます。
function toEuropeanNumber(n: string | null): string {if (typeof n === 'string') {// n is stringconsole.log(n.split(',').join('.'));return n.split(',').join('.');}// n is nullreturn 'null';}
この場合はreturn
は必須ではありません。
function toEuropeanNumber(n: string | null): string {if (typeof n === 'string') {// n is stringconsole.log(n.split(',').join('.'));} else {// n is nullreturn 'null';}}
typeof, instanceof
だけでは物足りず、自分でその変数が意図する型であるかを判定したくなることがあります。このようなときにType predicateを使うことができます。次の例は動物がアヒルかどうかを判定するその名もisDuck()
です。
function isDuck(animal: Animal): boolean {if (walksLikeDuck(animal)) {if (quacksLikeDuck(animal)) {return true;}}return false;}
ですが、この関数は使用者に対してその変数がアヒルかどうかを伝えているだけです。TypeScriptに対してそれがアヒルであることを伝えるためには型アサーションが必要になります。
if (isDuck(animal)) {const duck: Duck = animal as Duck;duck.quacks();// ...}
as
はTypeScriptにおけるキャストの一種です。名前を型アサーションと言いますが他の言語と異なりかなり強引な型変換ができてしまいますので使用には気をつけてください。
Type predicateは型アサーションをすることなくより賢くやろうものです。
Type predicateの宣言は戻り値がboolean
型の関数に対して適用でき、戻り値の型の部分を次のように書き替えます。
function isDuck(animal: Animal): animal is Duck {// ...}
これで関数isDuck()
がtrue
を返す時のif
のブロックの中ではanimal
はDuck
型として解釈されるようになります。
if (isDuck(animal)) {animal.quacks();// ...}
しかしながら、これはあくまでもその型であるとTypeScriptに解釈させるだけなので、JavaScriptとして正しいということは断言できません。
function isUndefined(value: unknown): value is undefined {return typeof value === 'number';}
上記関数isUndefined()
は明らかに誤っていますが、この誤りに対してTypeScriptは何も警告を出しません。
やりたいことはほぼType predicateと同じです。Type predicateはboolean
型の戻り値に対して使いましたがこちらは例外を投げるかどうかで判定します。上記関数isDuck()
をAssertion functionsで書きかえると次のようになります。
function isDuck(animal: Animal): asserts animal is Duck {if (walksLikeDuck(animal)) {if (quacksLikeDuck(animal)) {return;}}throw new Error('YOU ARE A FROG!!!');}// ...isDuck(animal);animal.quacks();
こちらはこの関数が呼ばれた後であればいつでも変数animal
はDuck
型として解釈されます。
値が存在するかしないかを表現するとき、言語によってはOptional
という入れ物のクラスを用意することがあります。このクラスを抽象クラスとして定義し、サブクラスに値が存在するSome
と存在しないNone
を用意するとOptional
にType predicateを使うことができます。
abstract class Optional<T> {public abstract isPresent(): this is Some<T>;}class Some<T> extends Optional<T> {public isPresent(): this is Some<T> {return true;}}class None<T> extends Optional<T> {public isPresent(): this is Some<T> {return false;}}
上記Optional
の例が顕著なのですが、optional.isPresent()
がfalse
を返したからと言ってTypeScriptは変数optional
がNone
であるとは解釈しません。あくまでもSome
ではないと解釈されるだけです。
if (optional.isPresent()) {// optional is Some<T>} else {// optional is something else other than Some<T>}
またType predicateはfalse
の場合を定義することができません。つまり次のような定義はできません。
public abstract isPresent(): this is Some<T>, this is not None<T>;
このような時は専用のメソッドを用意します。
abstract class Optional<T> {// ...public abstract isPresent(): this is Some<T>;public abstract isAbsent(): this is None<T>;}
ただしこの例の場合TypeScriptではユニオン型によって簡単に解決できます。ユニオン型については詳細の説明があるのでそちらをご参照ください。