TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – assert関数の型定義

TypeScript TypeScript
スポンサーリンク

ゴール:「assert 関数の型」を“道具として設計できる”ようになる

まずイメージからいきます。

assert 関数は一言でいうと、

「この条件が成り立たないなら、ここで止まってくれ」
「この時点で、値はもう〇〇型だとみなしていい」

という“宣言”を、コードと型の両方に刻み込むための関数です。

JavaScript 的には「条件を満たさなければ例外を投げる関数」。
TypeScript 的には「条件が true だった場合に、型を絞り込む関数」。

この 2 つの顔を同時に持っています。

ここでは、その「TypeScript 的な顔」、つまり

「assert 関数の型定義をどう書くと、型がどう振る舞うか」

を、例を交えながら丁寧に見ていきます。


まずは一番シンプルな assert 関数から

JavaScript 的な assert の形

まずは型を意識しない、素朴な assert から。

function assert(condition: unknown, message?: string) {
  if (!condition) {
    throw new Error(message ?? "Assertion failed");
  }
}
TypeScript

使い方はこうです。

function divide(a: number, b: number) {
  assert(b !== 0, "b must not be 0");
  return a / b;
}
TypeScript

b が 0 のときは例外を投げて処理を止める。
0 でなければそのまま進む。

JavaScript 的にはこれで十分ですが、
TypeScript 的には「assert のあとで b が 0 ではない」とは分かりません。

つまり、型の世界では「ただの void 関数」として扱われてしまいます。

TypeScript に「これは assert だ」と教える

ここで登場するのが、TypeScript の「アサーション関数(assertion function)」という仕組みです。

戻り値の型を、こう書き換えます。

function assert(condition: unknown, message?: string): asserts condition {
  if (!condition) {
    throw new Error(message ?? "Assertion failed");
  }
}
TypeScript

ポイントは : asserts condition という部分です。

これは、

「この関数が正常に戻った(例外を投げなかった)なら、
condition は true だとみなしてよい」

という意味の、特別な戻り値型です。

これで、TypeScript は「assert のあとでは条件が成り立っている」と理解できるようになります。


asserts condition が効くと何が嬉しいか

例1:null チェックを assert に任せる

次のようなコードを考えます。

function printUpper(text: string | null) {
  if (text === null) {
    throw new Error("text is null");
  }
  console.log(text.toUpperCase());
}
TypeScript

これはこれでいいのですが、
「null じゃないことを保証する」という役割を
assert 関数に任せることができます。

まず、assert 関数を定義します。

function assert(condition: unknown, message?: string): asserts condition {
  if (!condition) {
    throw new Error(message ?? "Assertion failed");
  }
}
TypeScript

これを使って書き換えると、こうなります。

function printUpper(text: string | null) {
  assert(text !== null, "text is null");
  console.log(text.toUpperCase());
}
TypeScript

ここで重要なのは、assert(text !== null, ...) のあとの行では、
text の型が string に絞り込まれていることです。

asserts condition によって、

「この行を通過した時点で、condition は true である」

と TypeScript が理解してくれるからです。

例2:配列の長さチェック

もう少しだけ複雑な例を見てみます。

function first<T>(arr: T[]): T {
  if (arr.length === 0) {
    throw new Error("empty array");
  }
  return arr[0];
}
TypeScript

これを assert で書き換えます。

function assert(condition: unknown, message?: string): asserts condition {
  if (!condition) {
    throw new Error(message ?? "Assertion failed");
  }
}

function first<T>(arr: T[]): T {
  assert(arr.length > 0, "empty array");
  return arr[0];
}
TypeScript

assert(arr.length > 0, ...) のあとの行では、
「arr.length > 0 が成り立っている」とみなされるので、
arr[0] にアクセスするのが「安全な操作」として扱われます。

ここでのポイントは、

「assert 関数を通過したあとの世界では、
その条件が“前提”として扱われる」

ということです。


もう一段深い型:asserts value is 型

「この値は〇〇型だ」と保証する assert

さっきの asserts condition は、

「条件が true であること」を保証する型でした。

もう一歩進んで、

「この値は T 型だ」と保証する assert も書けます。

例えば、Person 型を考えます。

type Person = {
  name: string;
  age: number;
};
TypeScript

外から来た値が Person かどうかをチェックし、
そうでなければ例外を投げる assert を作りたいとします。

型定義はこうなります。

function assertIsPerson(value: unknown): asserts value is Person {
  if (typeof value !== "object" || value === null) {
    throw new Error("not an object");
  }

  const v = value as { [key: string]: unknown };

  if (typeof v.name !== "string" || typeof v.age !== "number") {
    throw new Error("invalid person");
  }
}
TypeScript

戻り値の型 asserts value is Person は、

「この関数が正常に戻ったなら、value は Person 型だとみなしてよい」

という意味です。

使うとどうなるか

function greet(value: unknown) {
  assertIsPerson(value);
  // ここでは value は Person 型
  console.log(`Hello, ${value.name} (${value.age})`);
}
TypeScript

assertIsPerson(value) のあとの行では、
value の型が Person に絞り込まれています。

ユーザー定義型ガード(value is Person)と似ていますが、
こちらは「条件を満たさないと throw する」ことが前提になっている点が違いです。

型ガードは boolean を返す。
assert は「ダメなら throw、OK なら戻る」。

どちらも「型を絞り込む」ための道具ですが、
assert は「ここで絶対に条件を満たしていないと困る」という場面で使うイメージです。


asserts と never の関係を少しだけ

「条件が false のときは、ここで終わる」

asserts condition は、裏側では never と相性が良い概念です。

次のような関数を考えます。

function fail(message: string): never {
  throw new Error(message);
}
TypeScript

これを使って assert を書くと、こうなります。

function assert(condition: unknown, message?: string): asserts condition {
  if (!condition) {
    fail(message ?? "Assertion failed");
  }
}
TypeScript

failnever を返すので、
if (!condition) の中からは絶対に戻ってきません。

その結果、assert の外側では
「condition が true である世界」だけが残ります。

asserts condition という型は、
「false の世界は never に落ちて消える」
というイメージで捉えると、少ししっくり来るかもしれません。


実務での設計パターン

1. 「前提条件チェック」を assert にまとめる

関数の冒頭で、前提条件をチェックするコードが並ぶことがあります。

function process(user: User | null, config?: Config) {
  if (user === null) {
    throw new Error("user is required");
  }
  if (!config) {
    throw new Error("config is required");
  }

  // ここから先では user: User, config: Config
}
TypeScript

これを assert にまとめると、こうなります。

function assert(condition: unknown, message?: string): asserts condition {
  if (!condition) {
    throw new Error(message ?? "Assertion failed");
  }
}

function process(user: User | null, config?: Config) {
  assert(user !== null, "user is required");
  assert(config != null, "config is required");

  // ここから先では user: User, config: Config
}
TypeScript

「前提条件を満たさないならここで止める」
「ここから先では、前提条件が成り立っている」

という構造が、コードと型の両方にきれいに表現できます。

2. 「型チェック+エラー」を 1 つの assert に閉じ込める

外部データの検証では、
「型チェックして、ダメならエラー」という処理がよく出てきます。

function handleResponse(data: unknown) {
  if (!isPerson(data)) {
    throw new Error("invalid response");
  }

  console.log(data.name);
}
TypeScript

これを assert にすると、こう書けます。

function assertIsPerson(value: unknown): asserts value is Person {
  if (!isPerson(value)) {
    throw new Error("invalid response");
  }
}

function handleResponse(data: unknown) {
  assertIsPerson(data);
  console.log(data.name);
}
TypeScript

「型ガードで判定して、ダメなら throw」というパターンを
1 つの assert 関数に閉じ込めることで、
呼び出し側のコードがかなりスッキリします。


まとめ:assert 関数の型定義を自分の言葉で言うと

最後に、あなた自身の言葉でこう整理してみてください。

assert 関数は、

「条件を満たさなければ例外を投げて止める」
「条件を満たしているなら、その事実を型に反映する」

という 2 つの役割を持つ関数。

TypeScript では、

条件だけを保証したいときは
function assert(condition: unknown): asserts condition

特定の値の型を保証したいときは
function assertIsX(value: unknown): asserts value is X

のように書く。

設計するときは、

この時点で何を「前提」として扱いたいのか
その前提を満たさないときは、本当にここで止めていいのか

を一度考えてから、asserts を使う。

そうすると、assert 関数は
「ただのエラー投げ関数」から、
コード全体の前提条件と型の整合性を守る
小さな“門番”として機能し始めます。

タイトルとURLをコピーしました