60
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TypeScript: オブジェクト型のタイプガード関数を型安全にする小技

Last updated at Posted at 2020-03-24

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のロジックも修正しないとなりません。仮に、axに変わったとき、isFooaのままでも、valueFoo型であることをコンパイラは知らないので、その問題点を指摘してくれません。注意を払って目視でチェックしてればいい話ですが、そういう細かいのチェックはコンパイラにやってもらえると安心感が高まります。

なので、isFoovalueFoo型であることをコンパイラに伝えるコードを書きたいところです。

asで個々に型アサーションする

valueFoo型であることを、手っ取り早くコンパイラに伝えるようとすると、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の繰り返しが気になるのと、astring型と判明する前などに、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に書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:Twitter@suin

60
45
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?