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

TypeScriptのタイプガードは、タイプガード関数が問題なく実装されていれば、`as`で型アサーションするよりは、安全に型を狭めることができて、とても役に立つものです。

しかし、タイプガード関数自体が安全かどうかは、その実装次第なところがあります。

とりわけ、`object`型から具体的な型に狭めるタイプガード関数を作る場合は、型の安全性に不安が残りがちです。

たとえば、次の`Foo`のような型があったとき、

```typescript
interface Foo {
    a: string
    b: number
    c: boolean
}
```

`any`型からこの`Foo`型にするタイプガード関数`isFoo`を考えてみます。

```typescript
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`を使った型アサーションになります。

```typescript
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`のようなユーティリティタイプを用意して、

```typescript
type WouldBe<T> = { [P in keyof T]?: unknown }
```

それを型アサーションにはめていく形になります:

```typescript
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>`の型は、

```typescript
{
    a?: unknown
    b?: unknown
    c?: unknown
}
```

になるので、とても謙虚です。この型は、「オブジェクトで`a`,`b`,`c`のプロパティが有るかもしれないし、無いかもしれない、有ったとしてもどんな型かはまだ分からない(unknown)」という意味になります。


## 型アサーションの繰り返しをなくす

プロパティのチェックごとに型アサーションを繰り返し書くのもスッキリしないので、もうひと工夫します。

`isFoo`関数からオブジェクトか否かのチェックをしている2行を取り出し、`isObject`関数を作ります。この関数もタイプガード関数にして、`WouldBe<T>`型を推論できるようにしておきます:

```typescript
function isObject<T extends object>(value: unknown): value is WouldBe<T> {
    return typeof value === 'object' &&
        value !== null
}
```

あとは、`isObject`関数を`isFoo`関数に組み込みます:

```typescript
function isFoo(value: unknown): value is Foo {
    return isObject<Foo>(value) &&
        typeof value.a === "string" &&
        typeof value.b === "number" &&
        typeof value.c === "boolean"
}
```

だいぶスッキリした関数になったと思います。

## 完成形のコード

最後に完成形のコードを載せておきます。

```typescript
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](https://twitter.com/suin)