Edited at

TypeScript Handbook の Advanced Types をちょっとだけ掘り下げる - その1 Type Guards and Differentiating Types

More than 1 year has passed since last update.


はじめに

本記事は TypeScript HandbookAdvanced Types に書かれているものをベースに、説明されている内容をこういう場合はどうなるのかといったことを付け加えて ちょっとだけ 掘り下げます。完全な翻訳ではなく、若干元の事例を改変しています。

今回は Type Aliases について掘り下げます。

その2 Nullable types は こちら

その3 Type Aliases は こちら

その4 String Literal Types / Numeric Literal Types は こちら

その5 Discriminated Unions は こちら

その6 Index types は こちら

その7 Mapped types は こちら


Type Guards and Differentiating Types

以下のような二つの interface があったとして、

interface Bird {

fly();
layEggs();
}

interface Fish {
swim();
layEggs();
}

いずれかのインスタンスを返す以下のようなメソッドがあったとします。

function getSmallPet(): Fish | Bird {

// ...
}

そして、このメソッドを呼び出して返してきたインスタンスのメソッドをそれぞれ呼び出そうとすると以下のようになります。

let pet = getSmallPet();

pet.layEggs(); // 問題なし
pet.swim(); // コンパイルエラー
pet.fly(); // コンパイルエラー

Fish | BirdFishBird に共通している部分を抽出したものとして扱われるため、片側にしか存在しないメソッドは呼び出せません。これは以下のように書いた場合と同じ状況です。

interface EggsLayable {

layEggs();
}

interface Bird extends EggsLayable {
fly();
}

interface Fish extends EggsLayable {
swim();
}

function getSmallPet(): EggsLayable {
// ...
}

では、 swim()fly() を呼び出すためにはどうすればいいかということになるわけですが、インスタンスの型が何なのかを判定し、その判定した型として認識させる必要があります。

JavaScript でよくある判定方法はプロパティの存在有無ですが、 TypeScript の場合、存在しないプロパティにアクセスしようとしただけでコンパイルエラーとなるため単純には利用できません。

let pet = getSmallPet();

if (pet.swim) { // コンパイルエラー: アクセスしようとしただけでエラー
pet.swim(); // コンパイルエラー
} else {
pet.fly(); // コンパイルエラー
}

キャストを使って該当の型として認識させる必要があります。

let pet = getSmallPet();

if ((pet as Fish).swim) {
(pet as Fish).swim();
} else {
(pet as Bird).fly();
}


User-Defined Type Guards

上記のようなキャストを必要なたびにやるのは面倒です。

そこで TypeScript では Type Guard と呼ぶ、型の判定と呼び出しを簡単にする仕組みを用意しています。 Type Guard を定義するには Type Predicate と呼ぶものを戻り値とする関数を作るだけです。

function isFish(pet: Fish | Bird): pet is Fish {

return (pet as Fish).swim !== undefined;
}

pet is Fish 部分が Type Predicate で <parameterName> is <Type> という書き方をします。

if (isFish(pet)) {

pet.swim();
} else {
pet.fly();
}

これで呼び出し時のキャストが不要になり、簡単に書けるようになりました。

else 句内でのキャストまで不要になっていますが、これは TypeScript が else 句内は Fish | Bird から Fish を除いた Bird だと判断しているためです。

Type Predicate を返さずに単に boolean を返すような形だと Type Guard としては働きません。

function isFish(pet: Fish | Bird): boolean {

return (pet as Fish).swim !== undefined;
}

if (isFish(pet)) {
pet.swim(); // コンパイルエラー: Fish | Bird として認識される。
} else {
pet.fly(); // コンパイルエラー: Fish | Bird として認識される。
}

また、もう一つの利点として、

if ((pet as Fish).swim) {

(pet as Fish).swim();
} else {
(pet as Fish).swim(); // 2行上をコピペしたコード。実行時エラー
}

というようなミスが Type Guard を用いることで無くなります。

if (isFish(pet)) {

pet.swim();
} else {
pet.swim(); // 2行上をコピペしたコード。 Bird として判定されるため。コンパイルエラー。
}

型が三種類になった場合どうなるでしょうか。

interface Reptile {

walk();
layEggs();
}

function isFish(pet: Fish | Bird | Reptile): pet is Fish {
return (pet as Fish).swim !== undefined;
}

function getSmallPet(): Fish | Bird | Reptile {
// ...
}

let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly(); // コンパイルエラー: Fish を除いた Bird | Reptile として判定されている
}

もう一個 Type Guard を用意してやることで上手くいきます。

function isBird(pet: Fish | Bird | Reptile): pet is Bird {

return (pet as Bird).fly !== undefined;
}

let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else if (isBird(pet)) {
pet.fly();
} else {
pet.walk(); // (Fish | Bird | Reptile) - Fish - Bird = Reptile
}

なお、このようなパターンの Type Guard にはもっといいやり方があります。詳細は こちら


typeof type guards

primitive な型に対しての Type Guard を書くと typeof を用いた以下のような形になります。

function isNumber(x: any): x is number {

return typeof x === 'number';
}

function isString(x: any): x is string {
return typeof x === 'string';
}

function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(' ') + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}

なお、ここの例では コンパイラオプションstrictNullChecksfalse です。 strictNullChecks が true の場合、そもそも padding には string もしくは number しか来ないので例外処理は不要ですし、判定処理も単純になります。

function isNumber(x: any): x is number {

return typeof x === 'number';
}

function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(' ') + value;
} else {
return padding + value; // padding は string しかない
}
}

padLeft('Hello world', true); // コンパイルエラー
padLeft('Hello world', null); // コンパイルエラー
padLeft('Hello world', undefined); // コンパイルエラー

strictNullChecks については別記事で掘り下げます。

本題に戻りますが、このような primitive に対する Type Guard を作るのは面倒です。それに対して TypeScript は標準関数を別に用意をすることはせずに typeof x === 'number' という構文自体を Type Guard として認識するようにしました。

function padLeft(value: string, padding: string | number) {

if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value;
}
if (typeof padding === 'string') {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}

「Type Guard として認識されるのは typeof === '<typename>' もしくは typeof !== '<typename>' という二つの書き方をした時で、 typename は numberstringboolean もしくは symbol である必要があります。」

と原文には書いてありますが、 typeof が戻す値は上記以外にも functionobject があります。1 Type Guard に利用できないのでしょうか。

function getName(n: object | Function | string): Name {

if (typeof n === 'object') {
return n['name']; // エラーなし
} else if (typeof n === 'function') {
return n(); // エラーなし
} else {
return n;
}
}

できました...


instanceof type guards

instanceof を利用した Type Guard もあります。

instanceof はコンストラクタを用いて型の判別を行います。2

interface Padder {

getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(' ');
}
}

class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}

function getRandomPadder() {
return Math.random() < 0.5 ?
new SpaceRepeatingPadder(4) :
new StringPadder(' ');
}

// 型は 'SpaceRepeatingPadder | StringPadder'
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
padder; // 'SpaceRepeatingPadder' として認識される
}
if (padder instanceof StringPadder) {
padder; // 'StringPadder' として認識される
}

なお、配列の Type Guard は instanceof を用いて以下のようにできますが、

value instanceof Array

JavaScript の標準関数 Array.isArray も TypeScript では以下のように宣言されているため Type Guard として利用できます。

isArray(arg: any): arg is Array<any>;

また、 Underscore.js_.isArray などの型チェック関数も Type Guard として宣言されているので、そちらを利用することもできます。


まとめ

Type Guard を導入することでより安全により簡単に。





  1. MDN 



  2. 詳細は MDN を参照。