TypeScriptのis演算子を使った自作型ガードを使う

はじめに

こんにちは WESEEK でわりと何でもやっている haruhikonyan です。
みなさん TypeScript 書いてますか?
フロントエンドはもちろん Node でサーバサイドを書いてもよし、さらに型安全!
そんな型安全な TypeScript をより強固に使いこなすための User-Defined Type Guards の一つである is 演算子を使った自作型ガードの紹介をします。

型ガードとは

まず最初に型ガードとは何かです。
概要は参考 URL を読んでいただければいいんですが、おそらく一番使うだろうなのは以下のようなものかと思います。

type NullableString = string | null
const func = (nullableString: NullableString) => {
  if (nullableString === null) {
    // nullableString はこの if 文の中では null 型とトランスパイらは解釈する
    nullableString.length // 'nullableString' is possibly 'null'.(18047) いわゆるぬるぽ
    console.log("nullableString is null")
  } else {
    // nullableString はこの else 文の中では null 型ではないということなので string 型解釈する
    nullableString.length // string 確定なので length が参照できる
    console.log("nullableString is string")
  }
}

null や undefined チェックはよく使いますよね。
もちろん null みたいなプリミティブ型だけでなくユーザが定義したオリジナルの型などを判別したいことは往々にしてあるかと思います。
その時に登場するのが is 演算子です。

is とは?

説明するよりも見た方が早いと思うので先に実際の例を紹介します。
割と広く使われてるライブラリの一つである AxiosisAxiosError という簡単なものが実装されています。
使い方は にもある通り、何かしらの変数が AxiosError なのかどうかを判断かつ型ガードにより型の絞り込みをやってくれます。
そのまま紹介しようと思ったのですがなんと元のコードは JavaScript だったので比較的読みやすいようこちらで TypeScript に書き直しています。

// https://github.com/axios/axios/blob/56e9ca1a865099f75eb0e897e944883a36bddf48/lib/utils.js#L112
const isObject = (thing: any) => thing !== null && typeof thing === 'object';

// https://github.com/axios/axios/blob/1e58a659ec9b0653f4693508d748caa5a41bb1a2/index.d.cts#L74
class AxiosError<T = unknown, D = any> extends Error {
  // いろいろプロパティあるが割愛
}

// https://github.com/axios/axios/blob/56e9ca1a865099f75eb0e897e944883a36bddf48/lib/helpers/isAxiosError.js#L12-14
// https://github.com/axios/axios/blob/1e58a659ec9b0653f4693508d748caa5a41bb1a2/index.d.cts#L485
// 本体はこれ
function isAxiosError<T = any, D = any>(payload: any): payload is AxiosError<T, D> {
  return isObject(payload) && (payload.isAxiosError === true);
}

isObject

これも実質型ガードみたいなものですね is 演算子が使われてないのでトランスパイラ的に型ガードの型絞り込みは行ってくれませんが null ではないかつ typeof で object 型であることを保証してくれます。

isAxiosError

本題はこちらです。まず中身を見てみると

return isObject(payload) && (payload.isAxiosError === true);

とあり、まず payload が object であること。これはいいですね。
そしてその payload のプロパティに isAxiosErrortrue という値が入っていることを見ています。
これだけです。
Axios が発行するエラーのオブジェクトは必ず true が入っているということみたいですね。

function isAxiosError<T = any, D = any>(payload: any): payload is AxiosError<T, D>

関数の定義はこうなっており、返り値の型に注目してほしいのですが、これは  isAxiosError という関数が true を返すと 引数として与えられたpayload は(is) AxiosError<T, D> 型だと TypeScript のトランスパイラに伝えてあげるという意味になります。

説明

上記で説明したことがほぼ全てではありますが、以下 URL に詳しいちゃんとした仕様が書いてあるので迷った時や、もっと深く知りたい際には確認しましょう。

string 配列判定の関数を作ってみた

みなさん is 演算子については理解いただけたでしょうか。
ここでは実際に業務で TypeScript を書いており、とある変数が string の配列なのかどうかを正確に知りたくなった時に作成した関数の紹介をします。

作成した関数

export const isStringArray = (value: unknown): value is string[] => {
  return Array.isArray(value) && value.every((v) => typeof v === 'string')
}

上の説明を読んできた方ならもう読めるかと思いますが、説明をすると value として受け取ったものが、isArray にて配列であることかつ、
配列であることが確定した valueevery 関数によってすべての要素が string 型であれば string[] 型であるということをトランスパイラに教えてあげています。
実際の使いどころとしては、express で Request から query を受け取ると query の型は string | string[] | QueryString.ParsedQs | QueryString.ParsedQs[] | undefined というあらゆる可能性が考慮された型となります。
ただの string が欲しいのであれば typeof でいいのですが、string[] を確定させようとすると isArray だけでは不十分なので自作の型ガードを作成したという経緯になります。

気をつけないと型の偽りになるという話

is 演算子を使った型判定みなさんも作ってみたくなったことでしょう。しかし 定義に is Hoge と書いて関数が true さえ返してしまえばもうトランスパイラの中ではそれは Hoge 型というようになってしまうので適当な判別を書くと型の偽りになってしまいます。
isAxiosError の判定が決して適当という訳ではありませんが、適当なオブジェクトに isAxiosError というプロパティを持たせてそこに true を入れてしまえば AxiosError 型であるということになってしまいます。関数を定義する側も使う側もこういった危険性があることには留意しておいた方が良いかと思います。

終わりに

as や any に逃げずより型安全で堅牢なシステムを作ろう!