型ガード (Type Guards)

JavaScriptではtypeof, instanceofというその変数の型が何であるかを判別できる演算子があります。主にtypeofはプリミティブ型に、instanceofはクラスに対して使います。

TypeScriptはこれらの演算子で型を判別したあと、そのif, caseブロックではその変数はキャストをすることなくその型であるかのように振る舞うことができます。このTypeScriptの機能を型ガードと呼びます。以下はtypeofの例です。typeofswitchで使用し、その結果を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'の箇所です。ここで変数nunknown型からこのブロックに限りnumber型としてみなされnumber型のメソッドであるtoExponential()が使えるようになっています。

ユニオン型と併用する

変数がある型AまたはBであるようなとき、TypeScriptではユニオン型を使い表現できます。ユニオン型については専門のページがあるため詳しい説明はそちらに譲ります。ここでは型がAまたはBであるときはユニオン型を使うとA | Bと書くことができるということだけを覚えておいてください。

ユニオン型はそのどちらか(あるいはどれか)が不確定であることを示していますがtypeofまたはinstanceofを使うことによってそのユニオン型からどの型であるかを確定させることができます。

function toEuropeanNumber(n: string | null): string {
if (typeof n === 'string') {
// n is string
console.log(n.split(',').join('.'));
}
// ...
}

型が確定しているのはifブロックの中だけであり、そのブロックを抜けてしまうと変数は再び元の型として解釈されます。

function toEuropeanNumber(n: string | null): string {
if (typeof n === 'string') {
// n is string
console.log(n.split(',').join('.'));
}
// n is string | null
console.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 string
console.log(n.split(',').join('.'));
return n.split(',').join('.');
}
if (n === null) {
return 'NaN';
}
//return what?;
}

このようなときはifブロックの中でreturnが行われている、あるいはelseブロックが使われていればTypeScriptはそれ以外の場合を想定してくれます。

ifブロックの中でreturnをした場合

次の例のようにあるユニオン型からひとつの型を確定させ、確定させたifブロックでreturnが行われるとそのブロックを抜けたあとはユニオン型からその型を抜いたとしてTypeScriptは自動的に解釈してくれます。

function toEuropeanNumber(n: string | null): string {
if (typeof n === 'string') {
// n is string
console.log(n.split(',').join('.'));
return n.split(',').join('.');
}
// n is null
return 'null';
}

ifブロックに対応するelseブロックがある場合

この場合はreturnは必須ではありません。

function toEuropeanNumber(n: string | null): string {
if (typeof n === 'string') {
// n is string
console.log(n.split(',').join('.'));
} else {
// n is null
return 'null';
}
}

Type Predicate

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の宣言

Type predicateの宣言は戻り値がboolean型の関数に対して適用でき、戻り値の型の部分を次のように書き替えます。

function isDuck(animal: Animal): animal is Duck {
// ...
}

これで関数isDuck()trueを返す時のifのブロックの中ではanimalDuck型として解釈されるようになります。

if (isDuck(animal)) {
animal.quacks();
// ...
}

しかしながら、これはあくまでもその型であるとTypeScriptに解釈させるだけなので、JavaScriptとして正しいということは断言できません。

function isUndefined(value: unknown): value is undefined {
return typeof value === 'number';
}

上記関数isUndefined()は明らかに誤っていますが、この誤りに対してTypeScriptは何も警告を出しません。

Assertion Functions

やりたいことはほぼ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();

こちらはこの関数が呼ばれた後であればいつでも変数animalDuck型として解釈されます。

Type predicate, Assertion functionsのつかいかた

値が存在するかしないかを表現するとき、言語によっては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;
}
}

Type predicateで注意すること

上記Optionalの例が顕著なのですが、optional.isPresent()falseを返したからと言ってTypeScriptは変数optionalNoneであるとは解釈しません。あくまでも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ではユニオン型によって簡単に解決できます。ユニオン型については詳細の説明があるのでそちらをご参照ください。