ゴール:「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;
}
TypeScriptb が 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];
}
TypeScriptassert(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})`);
}
TypeScriptassertIsPerson(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");
}
}
TypeScriptfail は never を返すので、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 関数は
「ただのエラー投げ関数」から、
コード全体の前提条件と型の整合性を守る
小さな“門番”として機能し始めます。

