🍵

TypeScriptのtype predicateをライブラリなしで安全に使う

2022/04/20に公開

TypeScriptにはtype predicateという機能があります
しかしその機能を使うと型チェックが正しく行われないことがあるので、それを防ぐための書き方としてこういうのはどうですかという記事です   
ライブラリを使って安全に書く方法が下記の記事で紹介されています。とても充実したライブラリで、linterのカスタムルールも作成してくれているので、厳密に運用したい方は下記の記事を参考にするのが良さそうです👶
https://qiita.com/kgtkr/items/7e4f18224c3362ceceeb

一方で、諸事情からライブラリを使えない現場もあるかとは思います。そこで最近知った、ライブラリを使わない方法をここに残そうと思います!
ただもちろん、結局はコードの書き方を変えるに過ぎないので、厳密に運用する(=コードの書き手に強制する)には結局linterのカスタムルールが必要になります。「それならいいわ!」という方はここで引き返して下さい……すみません……😭

結論

TypeScriptのtype predicateの怖さは、関数内で用いられている型判定ロジックが間違っていてもコンパイルエラーが出ないことにあります
そこで、型がTかどうかを判定するtype predicateを作る際、判定ロジックを適用しながら、判定対象をT|null型変数へ代入することを行えば、そのような型のズレが防げると考えています👶

//Fish型かどうかを判定するtype predicate(Fish型は、後に引用する公式ドキュメントの例から採っています)
function isFish(pet: Fish | Bird): pet is Fish {
    const maybeFish: Fish | null =
        (pet as Fish).swim !== undefined ? pet : null
    return !!maybeFish
}

以下ではtype predicateの説明から、TypeScriptにおける問題とその解決方法(上記のコード)の解説をしていきます

(一応)type predicateとは何か

predicateという単語は日本語で「述語」を意味しますが、より狭くプログラミングの文脈においては「真偽判定をする関数」くらいに捉えていいと思っています👶(例えば下記のリンクは分かりやすいです)  
https://stackoverflow.com/questions/3230944/what-does-predicate-mean-in-the-context-of-computer-science

type predicateとは「型の判定をする関数」だと思っておけば良さそうです
もちろん、このような関数自体はTypeScriptだけでなくLISPなど他言語にも扱われているのですが、TypeScriptにおけるtype predicateというと特に下記のような関数を指します(公式ドキュメントより)

TypeScriptにおけるtype predicate
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

型を見てbooleanを返す関数の返り値の型をX is Tとすればよいわけです。これによって、関数で型ガードができるようになります(公式ドキュメントより)

TypeScriptにおけるtype predicateの使用例
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
 
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}

typeofinstanceofではできない型ガードを実現できるというわけです

安全でないtype predicate

便利なtype predicateですが、TypeScriptにおいては安全でない書き方ができてしまいます。具体的には「引数の型を判定するロジックが誤っていたらまずい」ということです
その例を冒頭でもご紹介させて頂いたこちらの記事から引用させて頂きます

例えば以下のコードは実際はnumber型であるかを判定する関数なのに、型上ではstring型であるかを判定する関数になってしまっている

TypeScriptにおける安全でないtype predicateの例(上記サイトより引用)
function isString(x: unknown): x is string {
  return typeof x === "number";
}

string型だと判定するためのロジックがnumber型の判定を行うロジックになってしまっているのです。このようにtype predicate内の型判定ロジックが誤っていたらまずいのです。また、これはコンパイルエラーが出ません👶
このような単純化された例では「そんな間違いある??」と思われるかもしれませんが、複雑なコードになるとありうる話なのでこれは困りました

安全にtype predicateを書く

では具体的にはどうしたらいいのでしょうか。ここからは冒頭で書いた結論の再説明になります
ポイントは、型がTかどうかを判定するtype predicateを作る際、判定ロジックを適用しながら、判定対象をT|null型変数へ代入することです。やり方は色々ありえますが、とりあえず三項演算子で実現すると下記のようになります

判定ロジックを適用しながら、判定対象をT|null型変数へ代入するtype predicate
function isType(x: 色々なタイプ): x is Type {
    const maybeType: Type | null =
        xがTypeのときだけTrueになる条件 ? x : null
    return !!maybeType
}

怖いのは「xがTypeのときだけTrueになる条件」が誤っているときです。Type型の判定をしているつもりが、TypeA型とかTypeB型になっているのが怖いのです
そこで、上記の書き方をすれば、もしTypeA型とかTypeB型になってしまうロジックでも、maybeTypeには三項演算子でnullが代入されて、結果はfalseに落ち着くというわけです👶

下記のように典型的な書き方と比較すると差が明確です

//典型的な書き方では、条件が間違っていたらtype predicateもつられて間違った結果を返すことになる
function isType(x: 色々なタイプ): x is Type {
    return xがTypeのときだけTrueになる条件
}

//今回の書き方では、条件が間違っているならtype predicateはそれを検知できる(falseを返すことができる)
function isType(x: 色々なタイプ): x is Type {
    const maybeType: Type | null =
        xがTypeのときだけTrueになる条件 ? x : null
    return !!maybeType
}

厳密に運用するにはカスタムルールが必要

とは言え、この書き方を強制させるにはlinterのカスタムルールのようなものが必要でしょうから、厳密に運用するなら冒頭でご紹介したライブラリとカスタムルールを使うのがいいかもしれません👶
本稿の手法は、日頃からお手軽にできる方法としてTips的に取り入れてみて下さい!

GitHubで編集を提案

Discussion