TypeScript 3.7 で追加された Assertion Functions を使って null チェックを楽にする

みなさん null チェックしてますか ? ぼくは今日も元気に null チェックしています。例えば渡された id で配列を走査してそのプロパティを返したいとき、ありますよね。次のようなコードです。

interface Item {
  id: number
  name: string
}

function getName(items: Array<Item>, id: number) {
  const target = items.find(item => item.id === id)
  return target.name  // error
}

ただし、このままでは TypeScript によってエラーが出力されてしまいます。Array.prototype.findundefined が返ってくる可能性があるからです。 このエラーをなくすために null チェックしましょう。

function getName(items: Array<Item>, id: number) {
  const target = items.find(item => item.id === id)
  if (target == null) {
    throw new Error('this path must not be reached')
  }
  return target.name
}

値が null だった場合は例外を飛ばしてその後のコードが実行されないようにしています。こうすると TypeScript コンパイラーは null でない型に確定したとして、エラーを出さなくなります。例外メッセージで表現していますが、存在しない id が渡されないことを関数外部で保証している想定です、少なくとも production 環境では。

さて、 null チェックをするにあたっていちいち if 文を書いて、例外を定義して、というのは非常につらみがあります。なにより長い。コードの見通しが悪くなってしまいます。

この課題は TypeScript 3.7 で追加された Assertion Functions を使うことによって解決されます。

function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
  if (val === undefined || val === null) {
    throw new Error(
      `Expected 'val' to be defined, but received ${val}`
    );
  }
}

function getName(items: Array<Item>, id: number) {
  const target = items.find(item => item.id === id)
  assertIsDefined(target)
  return target.name
}

assertIsDefined 関数を定義し、 null チェックの部分で使っています。ポイントは 2 つ。

  1. 返り値の型が asserts val is NonNullable<T>になっている。
  2. 条件に違反した場合、例外を飛ばしている

assertIsDefined 関数から何かしら値が返った場合、つまり何も例外が飛ばなかった場合、 TypeScript は引数として渡されたものを is の後ろに書いた型に確定させるという挙動をします。ここでは引数に渡されたものが null もしくは undefined でない場合、 NonNullable<T> に確定させてくれます。 if 文を用いたコードに比べると読みやすいですね。

例として assertIsDefined を挙げましたがこれは TypeScript 公式ドキュメントに書かれているものです。他にこれを汎用化した assert 関数も紹介されています。

function assert(condition: any, msg?: string): asserts condition {
  if (!condition) {
    throw new AssertionError(msg)
  }
}

Node.js で定義されている assert と同様です。が、 condition に指定する式を間違えるとバグを生みますので、使用には注意しましょう。

function a(src: any) {
  assert(typeof src === 'string')
  src.toUpperCase()
}

a(42) // no errors!!

こういった悲劇を防ぐために、 assertIsDefined のようにできるだけ用途を限定した方が良いでしょう。

TypeScript 3.7 は他にも色々便利なものが追加されています。上手に使っていきましょう。

TypeScript 3.7.2がリリースされました!Optional ChainingやNullish Coalescingをさっそく使ってみた