1. ホーム
  2. TypeScript

【TypeScript】アサーション関数(asserts)の使い方|条件を満たさなければ例外を投げて型を絞り込む

Share

実行時に値をチェックして「条件を満たさなければ例外を投げる」処理はよく書きますが、そのチェックを TypeScript の型にも反映させたいときに役立つのがアサーション関数(assertion functions)です。戻り値の型注釈に asserts というキーワードを書くと、「この関数が例外を投げずに戻ってきたなら、指定した条件は成り立っている」と TypeScript に伝えられ、呼び出し以降で型が絞り込まれます。この記事では、asserts condition の基本形、特定の型へアサートする asserts value is 型null / undefined を弾く実践例、そして型ガード(is)との違いまで、初心者〜中級者向けに解説します。

なぜアサーション関数が必要なのか

実行時のチェックと TypeScript の型は、放っておくと別々に動きます。たとえば「値が数値でなければエラーにする」という if を書いても、それを別の関数に切り出した瞬間、TypeScript はその関数の中で何をチェックしたのかを知りません。次のコードを見てください。

without-asserts.ts
function checkNumber(value: unknown): void {
  if (typeof value !== "number") {
    throw new Error("数値ではありません");
  }
}

function double(value: unknown): number {
  checkNumber(value);
  // checkNumber を通ったのに value はまだ unknown のまま
  return value * 2; // エラー:unknown 型は演算に使えない
}

checkNumber(value) が例外を投げずに戻ってきた時点で、value は数値のはずです。しかし checkNumber の戻り値の型は void なので、TypeScript は「チェックが済んだ」という事実を受け取れず、valueunknown のままです。この「実行時のチェック結果を型に橋渡しする」役割を担うのが、アサーション関数です。

基本の形:asserts condition

もっとも基本的なのは、引数で受け取った条件式(真偽値)をアサートする形です。戻り値の型注釈に asserts とパラメータ名を書きます。TypeScript 3.7 で導入された構文です。

assert.ts
function assert(condition: unknown, message?: string): asserts condition {
  if (!condition) {
    throw new Error(message ?? "アサーションに失敗しました");
  }
}

function getLength(value: string | null): number {
  assert(value !== null, "value が null です");
  // ここから先、value は string に絞り込まれる
  return value.length;
}

戻り値の型注釈 asserts condition は、「この関数が正常に戻ってきたなら、引数 condition は真である」という意味です。assert(value !== null, ...) を呼んだ後の行では、TypeScript は value !== null が成り立っていると考えるため、value の型から null が取り除かれ string に絞り込まれます。関数の中身は「条件が偽なら throw する」だけですが、型注釈のおかげで呼び出し側の型が変わるのがポイントです。

特定の型へアサートする:asserts value is 型

asserts condition は「条件が真」を伝えるだけですが、asserts 引数 is 型 と書くと、その引数を特定の型として断言できます。冒頭の checkNumber をアサーション関数に書き換えてみます。

assert-is-number.ts
function assertIsNumber(value: unknown): asserts value is number {
  if (typeof value !== "number") {
    throw new Error("数値ではありません");
  }
}

function double(value: unknown): number {
  assertIsNumber(value);
  // ここから先、value は number に絞り込まれる
  return value * 2;
}

戻り値の型注釈 asserts value is number は、「この関数が戻ってきたなら、引数 valuenumber である」という意味です。assertIsNumber(value) を通過した後の valuenumber として扱われるので、value * 2 がエラーなく書けます。関数本体で実際に typeof value !== "number" をチェックして例外を投げているため、型と実行時の振る舞いが一致しているのも重要な点です。

実践:null / undefined を弾く assertIsDefined

アサーション関数がよく使われるのが、値の存在チェックです。document.querySelector() や配列の find()、環境変数など、「あるはずだが型の上では null / undefined になりうる」値を安全に扱いたい場面です。汎用の assertIsDefined を用意しておくと便利です。

assert-is-defined.ts
function assertIsDefined<T>(
  value: T,
  message?: string
): asserts value is NonNullable<T> {
  if (value === null || value === undefined) {
    throw new Error(message ?? "値が存在しません");
  }
}

function focusInput(): void {
  const input = document.querySelector("input"); // 型は HTMLInputElement | null
  assertIsDefined(input, "input が見つかりません");
  // ここから先、input は HTMLInputElement に絞り込まれる
  input.focus();
}

asserts value is NonNullable<T> と書くことで、アサート後の値から nullundefined が取り除かれます。NonNullable<T> は TypeScript の組み込みユーティリティ型で、T から nullundefined を除いた型を作ります。querySelector の戻り値は HTMLInputElement | null ですが、assertIsDefined(input) を通した後は HTMLInputElement になり、input.focus() がエラーなく呼べます。if (input === null) return; のような分岐を毎回書く代わりに、1行で「無ければ例外、あれば型を絞り込む」を実現できます。

型ガード(is)との違い

戻り値の型注釈に is を使う型ガード(ユーザー定義型ガード)と、アサーション関数はよく混同されます。どちらも型を絞り込む仕組みですが、絞り込みのタイミングと使い方がまったく違います。

比較項目型ガード(value is Tアサーション関数(asserts value is T
戻り値boolean を返す何も返さない(void
絞り込み方if などの分岐の中で絞り込む呼び出した次の行から絞り込む
条件が偽のときfalse を返す(分岐で処理を分ける)例外を throw して処理を止める
主な用途型に応じて処理を分岐したいとき「満たさなければ続行不可」を保証したいとき

同じ「値が文字列か」を判定するコードを、両方の書き方で比べてみます。

guard-vs-assert.ts
// 型ガード:boolean を返し、if の中で絞り込む
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function useGuard(value: unknown): void {
  if (isString(value)) {
    // この if ブロックの中でだけ value は string
    console.log(value.toUpperCase());
  }
  // ブロックを抜けると value は unknown に戻る
}

// アサーション関数:満たさなければ throw し、以降ずっと絞り込む
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error("文字列ではありません");
  }
}

function useAssert(value: unknown): void {
  assertIsString(value);
  // この行以降、value はずっと string
  console.log(value.toUpperCase());
}

型ガード isStringboolean を返すので、if (isString(value)) のブロックの中でだけ valuestring に絞り込まれます。条件が偽なら分岐で別の処理を書けます。一方アサーション関数 assertIsString は、条件を満たさなければ throw して処理を止めるため、呼び出した次の行からは「必ず string である」前提でコードを書けます。「不正なら中断してよい」場面ではアサーション関数、「型ごとに処理を分けたい」場面では型ガード、と使い分けると整理できます。

アサーション関数でつまずきやすいところ

アサーション関数は通常の関数と少し違う制約があり、書き方を誤ると期待どおりに動きません。よくある2つの落とし穴を押さえておきましょう。

戻り値の型注釈を省略できない

通常の関数は戻り値の型を書かなくても推論されますが、アサーション関数の asserts ...明示的に型注釈を書かないと有効になりません。TypeScript は関数本体から asserts を推論しないためです。特にアロー関数に代入する場合は、変数側に型注釈が必要です。

arrow-assert.ts
// アロー関数では、変数の型注釈として asserts を書く
const assertIsString: (value: unknown) => asserts value is string = (
  value
) => {
  if (typeof value !== "string") {
    throw new Error("文字列ではありません");
  }
};

アロー関数の場合、const assertIsString: (value: unknown) => asserts value is string = ... のように、変数の型として asserts を含む関数型を明示します。関数式の右側だけに書いても効かない点に注意してください。関数宣言(function)で書けば、戻り値の位置に直接 asserts を書けるので、迷ったら関数宣言を使うのが簡単です。

チェック内容とアサートする型がずれている

アサーション関数は、asserts value is T と書けば TypeScript がそれを信じてしまいます。実際の関数本体のチェックが型と食い違っていても、コンパイラは警告しません。たとえば asserts value is string と宣言しながら、本体では数値かどうかをチェックしているといったミスは、型システムでは検出できず実行時のバグになります。アサートする型と、本体で throw する条件が正しく対応しているかは、書いた本人が責任を持って確認する必要があります。

まとめ

アサーション関数は、戻り値の型注釈に asserts を書くことで「この関数が例外を投げずに戻ってきたなら、指定した条件や型が成り立っている」と TypeScript に伝える仕組みです。asserts condition は条件式が真であること、asserts value is 型 は引数が特定の型であることを断言し、呼び出した次の行から型が絞り込まれます。null / undefined を弾く assertIsDefined のように、実行時のバリデーションと型を橋渡しできるのが強みです。boolean を返して分岐で使う型ガード(is)とは異なり、条件を満たさなければ throw して処理を止め、以降ずっと絞り込む点が特徴です。戻り値の型注釈は省略できず、チェック内容と型の一致は自分で保証する必要がある点にだけ注意して、実行時チェックと型を安全につなげてみてください。

参考ページ