Help us understand the problem. What is going on with this article?

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

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

suin
Qiita 4位/TypeScript入門書執筆中/TypeScripterのための座談会「YYTypeScript」主催/『実践ドメイン駆動設計』書籍邦訳レビュア/分報Slack考案/YYPHP主催/CodeIQマガジン執筆/株式会社クラフトマンソフトウェア創設/Web自動テスト「ShouldBee」の開発/TypeScript/DDD/OOP
https://yyts.connpass.com/
craftsman_software
「インフラの心配は、もうおしまい」 インフラ運用を自動化し、手作業を限りなくゼロにする会社
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away