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

# はじめに

本記事は [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/basic-types.html) の [Advanced Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html) に書かれているものをベースに、説明されている内容をこういう場合はどうなるのかといったことを付け加えて *ちょっとだけ* 掘り下げます。完全な翻訳ではなく、若干元の事例を改変しています。
今回は **Type Aliases** について掘り下げます。

その2 Nullable types は [こちら](https://qiita.com/kobanyan/items/c574ef8a20cc0590f739)
その3 Type Aliases は [こちら](https://qiita.com/kobanyan/items/58134cac1f92e31595c2)
その4 String Literal Types / Numeric Literal Types は [こちら](https://qiita.com/kobanyan/items/190b5bb926fbeeeaebe3)
その5 Discriminated Unions は [こちら](https://qiita.com/kobanyan/items/ca56df27de50ec267995)
その6 Index types は [こちら](https://qiita.com/kobanyan/items/dcdf12bbd8b793fe5bd7)
その7 Mapped types は [こちら](https://qiita.com/kobanyan/items/2fdd6b0ed7411ff9980e)

# Type Guards and Differentiating Types

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

```typescript
interface Bird {
  fly();
  layEggs();
}

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

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

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

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

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

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

```typescript
interface EggsLayable {
  layEggs();
}

interface Bird extends EggsLayable {
  fly();
}

interface Fish extends EggsLayable {
  swim();
}

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

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

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

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

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

```typescript
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** と呼ぶものを戻り値とする関数を作るだけです。

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

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

```typescript
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}
```

これで呼び出し時のキャストが不要になり、簡単に書けるようになりました。
else 句内でのキャストまで不要になっていますが、これは TypeScript が else 句内は `Fish | Bird` から `Fish` を除いた `Bird` だと判断しているためです。

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

```typescript
function isFish(pet: Fish | Bird): boolean {
  return (pet as Fish).swim !== undefined;
}

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

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

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

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

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

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

```typescript
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 を用意してやることで上手くいきます。

```typescript
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 にはもっといいやり方があります。詳細は [こちら](https://qiita.com/kobanyan/items/ca56df27de50ec267995) 。

## `typeof` type guards

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

```typescript
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}'.`);
}
```

なお、ここの例では [コンパイラオプション](https://www.typescriptlang.org/docs/handbook/compiler-options.html) の `strictNullChecks` は `false` です。 strictNullChecks が `true` の場合、そもそも `padding` には `string` もしくは `number` しか来ないので例外処理は不要ですし、判定処理も単純になります。

```typescript
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 については[別記事](https://qiita.com/kobanyan/items/c574ef8a20cc0590f739)で掘り下げます。

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

```typescript
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` は　`number` 、 `string` 、  `boolean` もしくは `symbol` である必要があります。」

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

[^1]: [MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof)

```typescript
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]

[^2]: 詳細は [MDN](https://developer.mozilla.org/ja/docs/JavaScript/Reference/Operators/instanceof) を参照。

```typescript
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 を用いて以下のようにできますが、

```typescript
value instanceof Array
```

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

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

また、 [Underscore.js](https://underscorejs.org/) の `_.isArray` などの型チェック関数も Type Guard として宣言されているので、そちらを利用することもできます。

## まとめ

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