👀

型の絞りこみ~型ガード関数編~【個人学習まとめ】

に公開

ユーザー定義の型ガード関数(型述語)

ユーザー定義の型ガード関数って?

開発者が独自に関数を定義して、その関数に渡された値が特定の型であるかどうかをチェックすることができます。
この関数のことをユーザー定義の型ガード関数と呼びます。
型ガード関数

ユーザー定義の型ガード関数を使わない型チェックとユーザー定義の型ガード関数を使った型チェックの両方から違いを確認してみましょう!

ユーザー定義の型ガード関数を使わない場合

まずはユーザー定義の型ガード関数を使わない場合です。

引数がnumber型かどうか判断する関数を準備します。typeof演算子を使用します。typeof演算子の戻り値はBoolean型でしたね。

function isNumber(value: unknown): boolean {
  return typeof value === "number";
}

次に異なるメソッドを呼び出して型チェックを行った結果を出力する関数を準備しますが・・・

function outValue(inputValue: number | string) {
  if (isNumber(inputValue)) {
    console.log(inputValue.toFixed());
    → プロパティ 'toFixed' は型 'string | number' に存在しません。
  プロパティ 'toFixed' は型 'string' に存在しません。
  } else {
    console.log(inputValue.toLowerCase());
    → プロパティ 'toLowerCase' は型 'string | number' に存在しません。
  プロパティ 'toLowerCase' は型 'number' に存在しません。
  }
}

toFixedメソッドとtoLowerCaseメソッドを呼び出す際にエラーとなりました。

if (isString(inputValue)) {部分だけで考えると、関数isStringの戻り値はBoolean型なので、引数にnumber型を渡してあげればtrueが帰ってくるので、if 文の判定は真となり戻り値にstring型である情報を取得できるため問題なくtoFixedメソッドを呼び出すことができそうですよね。

なぜこのような結果になったのでしょうか。

それは TypeScript が分かっているのは関数isStringの戻り値がboolean型であるということだけだからです。関数isStringで絞り込まれた型の情報は、関数isStringのスコープから外れて瞬間に失われてしまうからです。

変数のスコープについてはこちら

簡単な例でその挙動を確認しましょう。

function function1() {
  let x: any;
  x = 123.456;
  console.log(x.toFixed());
  x = "ABCD";
  console.log(x.toLowerCase());

  return x;
}

上記のコードではany型の変数xを返す関数function1を用意しました。
変数xを宣言時に初期化せずに宣言したので、値を代入しても変数xの型はany型に拡大されています。

しかし、この関数function1を外部から呼び出して変数valueに代入してみると・・・

関数function1の戻り値がstring型と推論されました。
これは関数function1returnする直前で変数xに、文字列ABCDを代入しているため、このような型推論が行われました。

では、最初のコードをもう一度確認しましょう。

//ユーザー定義の型ガード関数を使わない場合
function isNumber(value: unknown): boolean {
  return typeof value === "number";
}

function outValue(inputValue: number | string) {
  if (isNumber(inputValue)) {
    console.log(inputValue.toFixed());
    → toFixedメソッドを呼び出し時にエラー
  } else {
    console.log(inputValue.toLowerCase());
    → toLowerCaseメソッドを呼び出し時にエラー
  }
}

関数isNumberのスコープ内では、引数valuenumber型であることを TypeScript は分かっています。
しかし、関数isNumberのスコープを外れると、TypeScript は戻り値のboolean型であることしか分かりません。

よって、関数outValue内で型の安全性を保つことができないため、エラーが発生します。

このような時にユーザー定義の型ガード関数を使って、関数isStringが型をチェックするための関数であることを TypeScript に教えてあげることができます!

ユーザー定義の型ガード関数を使う場合

ユーザー定義の型ガード関数を使う場合は、対象となる関数の戻り値の型を(引数名) is (型名)とすることによって指定できます。
(このことを型述語と呼びます。)

実際のコードで確認しましょう。

function isNumber(value: unknown): value is number {
  return typeof value === "number";
}

function outValue(inputValue: number | string) {
  if (isNumber(inputValue)) {
    console.log("number型です");
    console.log(inputValue.toFixed());
  } else {
    console.log("string型です");
    console.log(inputValue.toLowerCase());
  }
}

上記のコードでいうと、関数isNumberの戻り値部分value is numberが該当します。
この関数が真の値を返した場合に、その引数が指定した型であることを TypeScript の型チェッカーに教えます。
今回でいうと「関数isNumbertrueを返したら、引数valuenumber型である」ということになります。

このことから関数outValue内で関数isNumberが真として評価された場合は、TypeScript が引数inputValuenumber型である!と理解し、number型のメソッドであるtoFixedメソッドを安全に呼び出すことができます。
if 文が偽となれば、引数inputValuestring型であることが分かるので、string型のメソッドであるtoLowerCaseメソッドを安全に呼び出すことができます。

最後に

最後になってしまいましたが、TypeScript5.5 からは型述語を記述しないような関数でも型ガード関数として機能が発揮されるようになったようです!

サバイバルTypeScriptより抜粋
// 返り値の注釈がないこの関数は x is number という型述語で推論される
function noAnnotation(x: unknown) {
  return typeof x === "number";
}

記事を書いている時に見つけたので「読むのが遅かった~」という気持ちは出てきましたが、こういった歴史があった。ということを頭の片隅にあるとどこかで役に立つかもしれない!ということにして覚えておきたいですね!
最後までありがとうございました。それではノシ

Discussion