はじめに
こんにちは 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 とは?
例
説明するよりも見た方が早いと思うので先に実際の例を紹介します。
割と広く使われてるライブラリの一つである Axios に isAxiosError
という簡単なものが実装されています。
使い方は 例 にもある通り、何かしらの変数が 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
のプロパティに isAxiosError
が true
という値が入っていることを見ています。
これだけです。
Axios が発行するエラーのオブジェクトは必ず true
が入っているということみたいですね。
function isAxiosError<T = any, D = any>(payload: any): payload is AxiosError<T, D>
関数の定義はこうなっており、返り値の型に注目してほしいのですが、これは isAxiosError
という関数が true
を返すと 引数として与えられたpayload
は(is) AxiosError<T, D>
型だと TypeScript のトランスパイラに伝えてあげるという意味になります。
説明
上記で説明したことがほぼ全てではありますが、以下 URL に詳しいちゃんとした仕様が書いてあるので迷った時や、もっと深く知りたい際には確認しましょう。
- https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard
- https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
string 配列判定の関数を作ってみた
みなさん is 演算子については理解いただけたでしょうか。
ここでは実際に業務で TypeScript を書いており、とある変数が string の配列なのかどうかを正確に知りたくなった時に作成した関数の紹介をします。
作成した関数
export const isStringArray = (value: unknown): value is string[] => {
return Array.isArray(value) && value.every((v) => typeof v === 'string')
}
上の説明を読んできた方ならもう読めるかと思いますが、説明をすると value
として受け取ったものが、isArray
にて配列であることかつ、
配列であることが確定した value
を every
関数によってすべての要素が 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 に逃げずより型安全で堅牢なシステムを作ろう!