【実務・中級編】 アサーションシグネチャ(asserts構文) – TypeScript実践ガイド

やあ。今日も元気にコード書いてるかい?
「一通りTypeScriptの基本はマスターしたし、`interface`や`type`を組み合わせて複雑な型も組めるようになった。よし、中級者だ!」……と胸を張っている君にこそ、今日お話しするテーマはグッと刺さるはずだ。

現場でバリバリとフロントエンドを書いていると、APIからのレスポンスのバリデーションや、外部ライブラリから流れてくる`unknown`なデータのハンドリングに頭を抱えることが本当によくあるよね。
「この値、絶対にnullじゃないって分かってるのに、TypeScript君が『可能性を捨てきれません』って怒ってくる……!」
そんなとき、君はつい`as User`なんて生えたての型アサーション(type assertion)で無理やり型をねじ込んでいないかい?

ちょっと待ってほしい。それ、コードが育ってきたときにバグの温床になる「技術的負債」のショートカットだから。
今回は、そんな泥臭い型安全の悩みを一発で、しかも美しく解決してくれる奥の手「アサーションシグネチャ(`asserts`構文)」について、シニアの視点から徹底的に叩き込んでいこうと思う。

—

なぜ普通の型ガード(User-Defined Type Guards)だけでは物足りないのか?

まず、おさらいから入ろう。TypeScriptで未知のデータを安全に扱うための常道として、私たちは「ユーザー定義型ガード」を書いてきた。

// 従来のユーザー定義型ガード
function isString(value: unknown): value is string {
typeof value === ‘string’;
}

function processData(data: unknown) {
if (isString(data)) {
// ここでは string 型として扱える
console.log(data.toUpperCase());
}
}

この `value is string` という構文、非常に便利だ。`if`文のスコープ内において、コンパイラに「この条件を通ったなら、この変数はこの型だ」と教えてあげられる。

しかし、現場でこんなシチュエーションに出会ったことはないだろうか?

function validateForm(input: unknown) {
// フォームのバリデーション関数。エラーなら即座に例外を投げたい!
if (typeof input !== ‘object’ || input === null) {
throw new Error(‘Invalid input’);
}

// ここでバリデーションを通過したから、以降は input を安全なオブジェクトとして扱いたい……
// だが、毎度 if で囲うのはめんどくさい!ガード関数でサクッと例外を投げて先に進みたい!
}

「エラーだったら即座に処理を中断する(早期リターンならぬ、早期例外スロー)」というパターンだ。このとき、従来の型ガード関数だと、関数を呼び出した後のコードで、コンパイラが自動的に型を絞り込んでくれないというもどかしさがあった。

ここで登場するのが、TypeScript 3.7で導入されたアサーションシグネチャ(`asserts`構文)だ。

—

アサーションシグネチャ(`asserts`)の正体と、裏側の挙動

アサーションシグネチャとは一言で言うと、「この関数がエラーを投げずに無事に完了したということは、引数の値は絶対にこの型であると保証する」とTypeScriptの型チェッカーに誓約させる機能だ。

書き方はこうだ。戻り値の型を書く位置に `asserts ◯◯ is ✕✕` と記述する。

function assertIsString(val: unknown): asserts val is string {
if (typeof val !== ‘string’) {
throw new TypeError(`Expected a string, but got ${typeof val}`);
}
}

ブラウザやTypeScriptのコンパイラは裏側でどう処理しているか?

少し裏側の話をしよう。TypeScriptのコンパイラ(tsc)は、静的コード解析を行う際、AST(抽象構文木)を走査して制御フロー分析(Control Flow Analysis)を行っている。

通常の関数呼び出しは、その内部で何が行われているかコンパイラが完全に追跡しきれない場合がある(副作用の考慮など)。しかし、`asserts` がついた関数が呼び出された場合、コンパイラは「この関数が例外を投げずに生存して抜けた場合、その後のコードパスにおいては、指定された変数の型情報を強制的に書き換える(オーバーライドする)」という特別なルールを適用する。

つまり、ブラウザのランタイムが実際にコードを実行する際、`asserts` 関数内でエラーが起きなければ、その下にあるコード群は「型安全な世界線」へ強制テレポートさせられるわけだ。

—

現場で使える!実践的サンプルコード

百聞は一見に如かず。実務でよくある、APIからのレスポンスデータのバリデーションにアサーションシグネチャを適用した綺麗なサンプルを見てみよう。

エディタにそのまま貼り付けて挙動を確認できるように、丁寧にコメントを仕込んでおいたよ。

/

  • ユーザー情報の型定義

/
interface User {
id: number;
name: string;
email: string;
}

/

  • @desc 渡された値が User 型のオブジェクトであるかを検証するアサーション関数
  • 万が一、条件を満たさない場合は即座にエラーをスローし、
  • 通過した場合はそれ以降のスコープで User 型としての安全性を保証する。

/
function assertIsUser(value: unknown): asserts value is User {
// まずオブジェクトかつ null でないことを確認
if (typeof value !== ‘object’ || value === null) {
throw new TypeError(‘バリデーションエラー: 入力値がオブジェクトではありません。’);
}

// 必須プロパティの存在と型をチェック
const record = value as Record;

if (typeof record.id !== ‘number’) {
throw new TypeError(‘バリデーションエラー: id が存在しないか、数値ではありません。’);
}
if (typeof record.name !== ‘string’) {
throw new TypeError(‘バリデーションエラー: name が存在しないか、文字列ではありません。’);
}
if (typeof record.email !== ‘string’) {
throw new TypeError(‘バリデーションエラー: email が存在しないか、文字列ではありません。’);
}

// ここまで到達したということは、value は無事に User 型の要件を満たしている!
}

// ==========================================
// 実戦での使用例
// ==========================================

async function fetchUserData(userId: number) {
const response = await fetch(`/api/users/${userId}`);
const json: unknown = await response.json();

try {
// 1. ここでアサーション関数を実行
// もし不正なデータなら、ここで catch ブロックへジャンプする
assertIsUser(json);

// 2. 奇跡! catch されずにこの行に到達した瞬間、
// json は面倒な型キャストなしで堂々と User 型として扱える!
console.log(`ユーザー名: ${json.name}`);
console.log(`メールアドレス: ${json.email.toLowerCase()}`); // string 型のメソッドが補完される!

} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error(‘予期せぬエラーが発生しました’, error);
}
}
}

どうだろう?このスッキリ感。
`if (isUser(json))` のような冗長なネストを作る必要がなく、「ガードを潜り抜けた先は、完全に安全な世界」というフラットなメンタルモデルでコードを書くことができるんだ。

—

もう一つの強力なパターン:存在証明(`asserts val`)

実は、アサーションシグネチャには型を指定するパターンのほかに、「単にその値が `null` でも `undefined` でも無いこと(存在すること)」を保証する強力な構文もある。

/

  • 値が null または undefined でないことを表明する

/
function assertIsDefined(val: T): asserts val is NonNullable {
if (val === undefined || val === null) {
throw new Error(`値が存在しません (received: ${val})`);
}
}

// 実際の使用例
function handleElement(elementId: string) {
const element = document.getElementById(elementId);

// DOM要素が取れなかったら即死させる
assertIsDefined(element);

// ここから先、element は HTMLElement として扱える(null や undefined の可能性が消滅)
element.style.backgroundColor = ‘red’;
}

ReactやDOMを触っていると、「ここ、絶対要素あるはずなのに `element?.style` みたいにオプショナルチェーニング書かなきゃいけないの面倒だな……」と感じることがあるはずだ。そんなとき、この `assertIsDefined` を一枚挟むだけで、コードの可読性が劇的に向上する。

—

シニアからの実務アドバイスと注意点

アサーションシグネチャは非常に強力な武器だけど、現場で導入する際には以下のポイントに気をつけてほしい。

1. 「雑な as」の免罪符にしないこと
アサーション関数の中身が `value as User` と型アサーションでごまかされているだけの場合、ランタイムエラーを防ぐ防壁としての意味がなくなってしまう。必ず関数の中身は `typeof` やプロパティの存在チェックを書き、「ランタイムの事実」と「TypeScriptの型」を一致させよう。
2. 例外を投げる設計に最適化する
アサーション関数は「失敗したら処理を止める(例外を投げる)」という前提のフローコントロールに向いている。正常系・異常系を優しく分岐させたい場面では、これまで通りの通常の型ガード(`value is T`)を使い分けよう。

—

おわりに

型アサーション(`as`)でコードの安全性を妥協するのは、今日で終わりにしよう。
アサーションシグネチャ(`asserts`構文)を使いこなせるようになると、APIの境界線や、DOMの取得、外部ライブラリとの統合など、フロントエンドの「型が汚れるポイント」を劇的に美しく、かつ強固に守ることができるようになる。

チームのメンバーから「おっ、このコード、めちゃくちゃ型安全で読みやすいな」と言われること請け合いだ。
ぜひ、今日のタスクからあなたのプロジェクトに取り入れてみてほしい。

それじゃあ、また次のアーキテクチャ談議で会おう! Happy Hacking!

コメント

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