【TypeScript】型ガードを改めて整理する
どうもフロントエンドエンジニアのoreoです。
この記事では代表的な型ガードの方法について整理したいと思います。
型ガードとは、ある値に対して特定の型かどうかチェックし、その結果に応じて処理を分けることを指します。ユーザー定義型ガードや型ガードの変数代入は、知っておくと差がつきますね。
1 typeof演算子
typeof演算子は、typeof 式 のような形で式を評価し、その評価結果に応じて以下表「結果」のような文字列を返します。式がnullの場合に、”object"を返すというイレギュラーな動きをするので、その点注意です。

参考:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof
typeof演算子を使うと、ユニオン型に対して、型のチェックを行い、処理の分岐が可能です。例えば、下記のように引数の型がstring | numberのユニオン型の場合、number型であれば、toString()メソッドで文字列に変換するnumberToStirng関数のようなものを定義することができます。
function numberToStirng(value: string | number) {
  if (typeof value === "number") {
    return value.toString();
  }
  return value;
}
2 instanceof演算子
instanceof演算子は、値 instanceof クラスオブジェクトのような形で、値がクラスオブジェクトのインスタンスかどうか判定し、真偽値を返します。
instanceof演算子を使うと、特定のクラスのインスタンスかどうかをチェックして処理の分岐が可能です。以下の例では、dateが、Dateクラスのインスタンスの場合、月を出力します。
function getMonth(date: string | Date) {
  if (date instanceof Date) {
    console.log(date.getMonth() + 1);
  }
}
3 in演算子
in 演算子は、値 in オブジェクトのような形で、値が、オブジェクトのプロパティかどうかを判定し、その真偽値を返します。
in 演算子を使うと、下記のようにpetがbowプロパティを持つ場合は、dogオブジェクトのbowメソッドを呼び出すことができます。
type Dog = {
  bow: () => void;
};
type Cat = {
  myao: () => void;
};
const dog: Dog = {
  bow: () => console.log("bow")
};
const cat: Cat = {
  myao: () => console.log("myao")
};
const pet = (pet: Dog | Cat) => {
  if ("bow" in pet) {
    dog.bow();
  } else {
    cat.myao();
  }
};
4 ユーザー定義型ガード
4-1 概要
続いてはユーザー定義型ガードです。「1 typeof演算子」で用いたnumberToStirng関数で、引数valueの型をunknownにした下記例用いて説明します。
function numberToStirng(value: unknown) {
  if (typeof value === "number") {
    return value.toString();
  }
  return value;
}
上記は正常に機能しますが、下記のようにif文の条件式をisNumber関数として切り出すと、型のチェックが機能せず、value.toString()で、value: unknown、Object is of type 'unknown'としてエラーとなります。条件式で関数が呼び出された場合、TypeScriptはその関数の定義を見に行き、型の絞り込みをしてくれません。
const isNumber = (value: unknown): boolean => {
  return typeof value === "number";
};
function numberToStirng(value: unknown) {
  if (isNumber(value)) {
    return value.toString();  //エラー!Object is of type 'unknown'
  }
  return value;
}
型の絞り込みに関数を使いたい場合、ユーザー定義型ガードを使います。
ユーザー定義型ガードは、下記isNumber関数のように、is演算子を使用して、返り値の型を引数名 is 型とします。そして、関数の返り値がtrueの場合、引数名に与えられた値が型に絞り込みます。下記例で言うと、isNumber関数の返り値がtrueの場合に、引数名つまりvalueが、number型となります。
const isNumber = (value: unknown): value is number => {
  return typeof value === "number";
};
function numberToStirng(value: unknown) {
  if (isNumber(value)) {
    return value.toString();
  }
  return value;
}
ただし、ユーザー定義型ガードでバグ(実際の型との乖離)があっても、TypeScriptはそれに気付けず、型安全を破壊する可能性があるので、その点は注意が必要です。
4-2 具体的な利用例
利用例としては、配列から  nullやundefinedの型情報を削除する時にユーザー定義型ガードが活躍します。
例えば、下記のような配列arrayで、filterメソッドを用いて、nullとundefinedを排除したとします。値は排除できても、配列filtedArrayの型情報にはnullとundefinedが残ってしまいます。
const array = ["shinji", null, "asuka", "rei", undefined];
//型:const array: (string | null | undefined)[]
const filtedArray = array.filter((val) => val != null);
console.log(filtedArray); //["shinji", "asuka", "rei"]
//型:const filtedArray: (string | null | undefined)[]
※TypeScriptでは、val != nullで、nullとundefinedの両方をチェックできます。
このような場合、ユーザー定義型ガードを使えば、型情報からもnullとundefinedを排除することができます。
const filtedArray = array.filter((val): val is string => val != null);
//型:const filtedArray: (string)[]
4-3 (補足)NonNullable<T>
ちなみに、TypeScriptには、ユーティリティ型のNonNullable<T>があり、これを使うと型Tからnullとundefinedを排除できます。NonNullable<T>は、TypeScriptの内部実装的には下記のように実装されています。
type NonNullable<T> = T extends null | undefined ? never : T;
5 型ガードの変数代入
TypeScript4.4以降で使用可能な機能になりますが、型ガードに変数を使うことができます。
例えば、下記のようにdataがDateのインスタンスかどうかの型チェックの結果を、変数isDateに代入することができます。
function getMonth(date: string | Date) {
  const isDate = date instanceof Date;
  if (isDate) {
    console.log(date.getMonth() + 1);
  }
}
6 最後に
型ガードの変数代入は初めて知りました。知らない機能がどんどんアップデートされていくので、キャッチアップしていきたいです!
7 参考
Discussion