TypeScript: ユーザ定義の型ガード

これは記事の日本語版です。

The English version of this article is here.

最近、「ユーザ定義の型ガード」と言うTypeScript言語機能を発見しました。この機能で、関数の境界を超えつつ型情報を伝えることができます。

問題定義から始めます。

型の特殊化

TypeScriptでは、情報の存在を確認してから扱うことが多いです。

function plusOne(value: number) {
  console.log(value + 1)
}

let x?: number

// エラー! `x` は数字ではないかも。
plusOne(x)

if (x !== undefined) {

  // `x` はもう絶対に数字である。
  plusOne(x)
}

undefinedではないことを確認したことで、xの型が特殊化され、null安全が実現できました。

実践的には、xはクラスのメンバーの可能性もあります。存在条件を数回も書かされてしまいます。

class Example {

  value?: number

  plusOne() {
    if (this.value !== undefined) {
      console.log(this.value + 1)
    }
  }

  plusTwo() {
    if (this.value !== undefined) {
      console.log(this.value + 2)
    }
  }
}

現時点では、コードをDRYにしたくなります。共通コードを別の関数に抜いておきます。

class Example {

  value?: number

  checkValue(value?: number): boolean {
    return value !== undefined
  }
}

残念ですが、存在チェックは別の関数スコープで行われているせいで、TypeScriptは呼び出す側の型を特殊化してくれません

class Example {

  ...

  process() {
    if (this.checkValue(this.value)) {

      // エラー! `value` は `undefined` かも!!
      this.plusOne(this.value + 1)
    }
  }
}

ユーザ定義の型ガード

ユーザ定義の型ガードと言うアドバンスドTypeScript言語機能が上記の窮地を解いてくれます。

名前通り、引数の型の特殊化を戻り値にする機能です。例えば、先の存在チェックは次のように修正します。

class Example {

  value?: number

  checkValue(value?: number): value is number {
    return value !== undefined
  }
}

新しい構文は、戻り値がtrueであれば、引数のvalueundefinedではないことが保証されています。これでこの関数が共有できるようになりました。

まとめ

正直に言うと、TypeScriptが自動的に同期的な関数境界を超える型情報を保つべきだと私は思います。同期的なコードのインタラプトをサポートする環境のため、機能の有効をtsconfig.jsonで管理するのが理想です。

でもそうなるまで、これはもう1つの型安全の道具として使用させていただきます。