TypeScriptのタイプガードは、タイプガード関数が問題なく実装されていれば、as
で型アサーションするよりは、安全に型を狭めることができて、とても役に立つものです。
しかし、タイプガード関数自体が安全かどうかは、その実装次第なところがあります。
とりわけ、object
型から具体的な型に狭めるタイプガード関数を作る場合は、型の安全性に不安が残りがちです。
たとえば、次のFoo
のような型があったとき、
interface Foo {
a: string
b: number
c: boolean
}
any
型からこのFoo
型にするタイプガード関数isFoo
を考えてみます。
function isFoo(value: any): value is Foo {
return typeof value === 'object' &&
value !== null &&
typeof value.a === 'string' &&
typeof value.b === 'number' &&
typeof value.c === 'boolean'
}
この関数には大きな問題はありません。
しいて課題をあげるなら、例えばプロパティa
を別の名前に変更したときに、isFoo
のロジックも修正しないとなりません。仮に、a
がx
に変わったとき、isFoo
がa
のままでも、value
がFoo
型であることをコンパイラは知らないので、その問題点を指摘してくれません。注意を払って目視でチェックしてればいい話ですが、そういう細かいのチェックはコンパイラにやってもらえると安心感が高まります。
なので、isFoo
のvalue
がFoo
型であることをコンパイラに伝えるコードを書きたいところです。
as
で個々に型アサーションする
value
がFoo
型であることを、手っ取り早くコンパイラに伝えるようとすると、as
を使った型アサーションになります。
function isFoo(value: unknown): value is Foo {
return typeof value === 'object' &&
value !== null &&
typeof (value as Foo).a === 'string' &&
typeof (value as Foo).b === 'number' &&
typeof (value as Foo).c === 'boolean'
}
これでも十分感はありますが、as Foo
の繰り返しが気になるのと、a
がstring
型と判明する前などに、value as Foo
と決めてしまうのは、ちょっと決めつけすぎ感があります。
もう少し謙虚に型アサーションする
もう少し謙虚にいくなら、下記のWouldBe
のようなユーティリティタイプを用意して、
type WouldBe<T> = { [P in keyof T]?: unknown }
それを型アサーションにはめていく形になります:
function isFoo(value: any): value is Foo {
return typeof value === 'object' &&
value !== null &&
typeof (value as WouldBe<Foo>).a === 'string' &&
typeof (value as WouldBe<Foo>).b === 'number' &&
typeof (value as WouldBe<Foo>).c === 'boolean'
}
WouldBe<Foo>
の型は、
{
a?: unknown
b?: unknown
c?: unknown
}
になるので、とても謙虚です。この型は、「オブジェクトでa
,b
,c
のプロパティが有るかもしれないし、無いかもしれない、有ったとしてもどんな型かはまだ分からない(unknown)」という意味になります。
型アサーションの繰り返しをなくす
プロパティのチェックごとに型アサーションを繰り返し書くのもスッキリしないので、もうひと工夫します。
isFoo
関数からオブジェクトか否かのチェックをしている2行を取り出し、isObject
関数を作ります。この関数もタイプガード関数にして、WouldBe<T>
型を推論できるようにしておきます:
function isObject<T extends object>(value: unknown): value is WouldBe<T> {
return typeof value === 'object' &&
value !== null
}
あとは、isObject
関数をisFoo
関数に組み込みます:
function isFoo(value: unknown): value is Foo {
return isObject<Foo>(value) &&
typeof value.a === "string" &&
typeof value.b === "number" &&
typeof value.c === "boolean"
}
だいぶスッキリした関数になったと思います。
完成形のコード
最後に完成形のコードを載せておきます。
interface Foo {
a: string
b: number
c: boolean
}
function isFoo(value: unknown): value is Foo {
return isObject<Foo>(value) &&
typeof value.a === "string" &&
typeof value.b === "number" &&
typeof value.c === "boolean"
}
type WouldBe<T> = { [P in keyof T]?: unknown }
function isObject<T extends object>(value: unknown): value is WouldBe<T> {
return typeof value === 'object' &&
value !== null
}
最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします→Twitter@suin