はじめに
ユーザー定義型ガード、使ってますか?こんなやつです
const isString = (value: unknown): value is string => typeof value === 'string'
こういうやつを型安全に書けるライブラリを12行で作成しました
以下はその実装です
コピペでもいいですし、 predicatory という名前で npm に公開してるのでそこからインストールすることもできます
const excludeSymbol = Symbol()
const factory =
<I, O extends I>(
extract: (value: I) => O | typeof excludeSymbol
): ((value: I) => value is O) =>
(value: I): value is O =>
extract(value) !== excludeSymbol
export const guard = Object.assign(factory, {
exclude: excludeSymbol,
} as const)
次のように使うことができます
// これは `(value: unknown) => value is string` になる
const isString = guard(value => typeof value === 'string' ? value : guard.exclude)
ユーザー定義型ガードの問題点
なぜこんなものを作ろうと思ったかというと、ユーザー定義型ガードを自前で実装しようとすると問題があるからです
具体的には型安全ではありません
上に挙げた isString
関数(引数として受け取ったものが string
型かどうかを判定するユーザー定義型ガード)を例にすると、以下のように条件式を反転させてもコンパイラは怒ってくれません
- const isString = (value: unknown): value is string => typeof value === 'string'
+ const isString = (value: unknown): value is string => typeof value !== 'string'
なんなら typeof value === 'number'
とかにしてもコンパイルは通ってしまいますし、安全とは言えません
こんなシチュエーションも guard
関数を使えば型安全に書くことができます
// これは `(value: unknown) => value is number` になる
const isString = guard(value => typeof value === 'number' ? value : guard.exclude)
また、引数の型が予めわかっている場合( Array.filter()
など)にも対応しています
type Foo = {
foo: any
}
type Bar = {
bar: any
}
const arr: (Foo | Bar)[] = []
// `Foo[]` になる
const foos = arr.filter(guard(fooOrBar => 'foo' in fooOrBar ? fooOrBar : guard.exclude))
おわりに
スターをいただけると泣いて喜びます
https://github.com/myrear/predicatory