TypeScriptのType Predicateは興味深い構文機能です。それらはリターン型のアノテーションと同じ場所に現れますが、典型的なアノテーションよりも肯定的な文のように見えます。これにより、型チェックをより細かく制御することができます。

TypeScript 5.5のリリース以降、型プリディケートの操作はより直感的になりました。多くの場合自動的に推論することができるからです。しかし、もう少し古いコードベースを操作する場合、手書きの型プリディケートとの出会いがより頻繁になるでしょう。

この記事では、型プリディケートとは何か、なぜ有用なのかを簡単に探ります。まず、それが解決する問題を見ていきましょう。

問題

型プリディケートの有用性を理解する最好の方法は、それらがない場合に発生する問題に気づくことでしょう:

function isString(value: unknown): boolean {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string | number
  }
  return " ".repeat(padding) + input; // Opps type error here
                 //   ^
                 // string | number
}

ここで、isStringのリターン型はbooleanに設定されており、padLeftという関数内で使用しています。この関数は入力文字列の左側に余白を追加します。paddingは与えられた文字列または指定された数の空白文字かのどちらかです。

return typebooleanに直接コードする理由を疑惑しているかもしれません。これは問題の例示をするためです。return typeの注釈を追加しないと、最新のTypeScript版を使用してもここに何の問題も発見しません。しかし、このバージョンによる違いについては後で話します。

関数は実行時には問題なく動くことになりますが、isStringを使用して型の狭隘化を行えず、paddingの型がstring | numberとして、ifステートメントの中と外どちらでも変わらないままになります。これにより、repeatの最初の引数に対する期待する型との衝突を引き起こし、型のエラーを招きます。

解決策: 型判定を使用します

型判定という用語を知らないとも、以前に使用したことがあるかもしれません。プログラミング上の型判定は、返り値がbooleanであり、yes/noの質問に答える関数です。JavaScriptの内蔵の配列メソッドの多く、filterfindeverysomeなどは、判定を行うために型判定を使用しています。

型判定は型の狭隘化をより効果的にするために、判定をより便利にする手段として使用することができます。型判定を返り値として使用することで、問題を解決することができます。

function isString(value: unknown): value is string {
  return typeof value === "string";
}

ここでの型判定はvalue is stringです。これは3つの事柄を述べています。

  • 関数は判定関数であるため、TypeScriptは返り値がboolean以外を試みた場合にエラーを表示します。

  • もしtrueを返した場合、valueは文字列型です。

  • もしfalseを返した場合、valueは文字列型ではありません。

型 predicates を使用することで、ユーザー定義された型ガードを作成することができます。型ガードは、より特定の型に絞ることができる論理的なチェックです。つまり、上記の関数もユーザー定義の型ガードです。

以下が完全なコードです。

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string
  }
  return " ".repeat(padding) + input;
                 //   ^
                 // number
}

ここで、TypeScriptは if 文の中でも外でも padding の型を正しく狭めます。

次に、TypeScript 5.5 以前の型 predicates の機能と、このバージョンでどのように改善されたのか簡単に見ていきましょう。

Type Predicates Before TypeScript 5.5

先前的な例で、返り値の型を指定していない場合、推論される型は boolean です。

function isString(value: unknown) {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string | number
  }
  return " ".repeat(padding) + input; // Opps type error here
                 //   ^
                 // string | number
}

結果として、私たちは、手動で返り値の型をbooleanとして書いたときと同じエラーを得ました。上記のコード片段のTypeScriptプレイグラウンドリンクはこちらです。機能や変数をmouseoverして型の感覚を良くしてください。その後、型 predicate を書いて問題を解決する方法を見てください。

型 predicate を指定しない場合、filterなどの方法でも正しくない型の認識が発生します:

function isString(value: unknown) {
  return typeof value === "string";
}

const numsOrStrings = [1, 'hello', 2, 'world'];
//      ^
//    strings: (string | number)[]

const strings = numsOrStrings.filter(isString);
//      ^
//    strings: (string | number)[]

今はTypeScript 5.5が状況を改善しているのを見ていきましょう。

Type Predicates の後 TypeScript 5.5

TypeScript 5.5の最も重要な機能の1つは、関数体を分析して型 predicate を推論できるようにする機能です。したがって、TypeScript 5.5またはそれ以降を使用している場合、isStringの返り値として型 predicate を書く必要はありません。TypeScriptがそれを代わりに行い、以下の例のようなコードは完璧に機能します:

function isString(value: unknown) {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string
  }
  return " ".repeat(padding) + input; // Opps type error here
                 //   ^
                 // number
}

const numsOrStrings = [1, 'hello', 2, 'world'];

const strings = numsOrStrings.filter(isString);
//      ^
//    strings: string[]

const numbers = numsOrStrings.filter((v) => !isString(v));
//      ^
//    numbers: number[]

型述語の自動推論に不満を感じる状況にはまだ出会っていません。もし見つけた場合は、必ず独自に書き直すことができます。

さらなる学習

この記事では、TypeScriptにおける型述語について簡単に探求しました。さらに理解を深め、エッジケースを理解したい場合は、公式ガイドをご覧ください:

読んでくれてありがとう!次回までにお待ちください!

カバー写真の背景は、Mona EendraUnsplash上のものです。