
TypeScriptのType GuardsとType Predicatesをそれぞれ適切に使う方法
TypescriptのType Predicatesを理解してみようということで、
海外の記事を色々と探しておりました。
そうしましたら、TypeScript Type Guards and Type Predicates という記事を見つけまして、
なんとなく役に立ちそうだったので、これを翻訳しながら、分かりやすく、かいつまんで、まとめてお届けしたいと思います。
もし詳しく、全文を読みたければ、是非とも英語の記事を読んでみてください。
TypeScriptタイプガードとタイプPredicates
ユニオン型は、複数の異なる型のパラメータを受け入れることを可能にします。
x型またはy型のいずれかを呼び出し時に選べるわけですが、
正確な型に基づいて何らかのコードを実行したいと思うかもしれません。
ここで、タイプガードとタイププレディケートが登場します。
まずは、型の宣言から始めましょう。
私は何かを説明しようとするとき、コードを見るのが好きです。
そうすることで、コンセプトをよりよく理解できるからです。
ブログを作っていて、2つの型があり、それが1つのユニオンを形成しているとします。
type Article = {
frontMatter: Record<string, unknown>;
content: string;
}
type NotFound = {
notFound: true;
}
type Page = Article | NotFound;
具体的な型としては、ArticleとNotFoundがあります。
目標は、ページをレンダリングする関数を書くことです。
ブログが存在するかどうかをチェックする要件や、notFound関数をいつ呼び出すかなどの詳細については触れませんが、
単一のレンダリング関数があると想像してみてください。
データベースの内容に基づいて、記事をレンダリングするか、not foundページをレンダリングします。
こんな感じです。
function handleRequest(slug: string): Page {
const article = db.articles.findOne({ slug });
const page = article ?? { notFound: true };
return render(page);
}
ここでの課題は、
handleRequestがArticleとNotFoundのどちらを返したかを知る必要がある場合です。
JavaScriptでは、次のようなものを使います。
function render(page: Page) {
if (page.content) {
return page.content;
}
return '404 — not found';
}
しかし、TypeScriptではそれはうまくいきません。
プロパティの内容がPage型に存在しないことを示すErrorが投げられます。
Property 'content' does not exist on type 'Page'.
Property 'content' does not exist on type 'NotFound'.
これは、ユニオン内のすべての型にそのプロパティが含まれているわけではないからです。
これを解決するには、タイプガードを追加する必要があります。
Type Guard
タイプガードとは、現在のスコープ内の型を保証する実行時のチェックを行う式のことです。
手っ取り早い方法は、page.contentのチェックをTypeScriptが理解できるものに置き換えることだ。
function render(page: Page) {
if ('content' in page) {
return page.content;
}
return '404 — not found';
}
これは効果がありますが、メンテナンス性を犠牲にしています。
TypeScriptの利点は、使用されているプロパティを削除したときに警告してくれることです。
今回の変更により、例えばcontentプロパティの名前をbodyに変更しても、
TypeScriptは警告を出しませんし、あるいは、’content’にタイプミスがあったときにも警告話です。
そして、これがタイププレディケートが面白い理由です。
Type Predicate
型述語とは、次のような関数の戻り値の型のことです。
function isArticle(page: Page): page is Article {
return 'content' in page;
}
Type Predicateはpage is Articleです。
また、知っておいていただきたいのですが、page in ‘content’はこの文脈ではタイプガードではありません。
これは単純な表現です。
タイプガードとは、TypeScriptに型を絞らせるif文のことです。
つまり、上の関数は以前のタイプガードとよく似ていて、同じようにメンテナンス性の問題を抱えています。
しかし、それを抽出したからには、それを解決することもできます。
function isArticle(page: Page): page is Article {
return typeof (page as Article).content !== 'undefined';
}
これは、Articleをリファクタリングしてcontentプロパティを削除するとエラーになります。
Type Predicateとして宣言された関数は、ブール値を返さなければなりません。
戻り値がtrueの場合、TypeScriptは戻り値の型がType Predicateで宣言されているものであると仮定し、
TypeScriptは提供された引数ページがArticle型であると仮定します。
このメソッドをrender関数内で呼び出すと、こんな感じになる。
function render(page: Page) {
if (isArticle(page)) {
return page.content;
}
return '404 — not found';
}
TypeScript は page.content が存在することを知っています。
なぜなら if スコープ内では page は Article 型だからです。if (isArticle(page)) 式は、タイプガードです。
if文の後、pageはArticle型ではないならば、
ユニオンには2つの型しかないので、TypeScriptはその段階でNotFound型でなければならないことも認識しているということですね。
以上!とても勉強になりました。