【実務・中級編】関数の戻り値に「void」を指定する真の意味と、呼び出し元での無視の挙動 – TypeScript コア・型システムの基礎解析バイブル

開発チームの皆さん、コードレビューお疲れ様です。テクニカルリードの私だ。

今回は、TypeScriptにおける極めて基礎的でありながら、多くの中堅エンジニアですを見落としがちな闇——「関数の戻り値における `void` の真の挙動と、呼び出し元での型の安全性」について徹底的に解説する。

「あ、関数が何も返さないから `void` をつければいいんでしょ」
「`return` しなければコンパイラは文句言わないし問題ないよね」

もし君がそう考えているなら、TypeScriptの型システムの本質を見誤っている。今回は、なぜ `void` を返す関数が実際には値を返せてしまうのかという言語仕様の背景から、実務のフロントエンド開発や非同期API連携でバグを踏まないための「堅牢な設計パターン」まで、コードの裏側の挙動を交えてロジカルに伝授しよう。

—

1. なぜ `void` を返す関数が「値を返せてしまう」のか?

まずは、TypeScriptのコンパイラが `void` をどう評価しているかという、言語の深層に迫る。

TypeScriptの `void` は、JavaScriptの実行時における `undefined` とイコールではない。型システムにおける `void` は、「呼び出し元がその関数の戻り値を利用してはならない(無視すべきである)」という制約を表現するためのものだ。

ここで、以下のコードを見てほしい。

// 意図:何も返さない(void)ロギング関数
function logAndReturn(message: string): void {
console.log(message);
return 42 as any; // 暴挙:あえて値を返してみる
}

// 呼び出し側
const result: void = logAndReturn(“Hello, TypeScript”);
// コンパイルエラーにならない!
// しかも実行時には 42 が返ってきてしまっている。

「えっ、`void` なのに値を返せるの?」と思っただろう。
実はこれ、TypeScriptの設計上の仕様(あるいはJavaScriptとの互換性のための意図的な緩み)なのだ。TypeScriptでは、戻り値の型が `void` の関数は、実際に何を `return` していようがコンパイルエラーにならないというサブタイピング規則が存在する。

さらに、コールバック関数を渡す場面(高階関数)において、この挙動はさらに牙をむく。

type VoidCallback = () => void;

// 呼び出し側で、明示的に値を返す関数を渡してみる
const processCallback = (callback: VoidCallback) => {
const res = callback(); // ここで戻り値を期待してはいけないが…
console.log(res); // 実行時、もしコールバックが値を返していれば出力されてしまう
};

processCallback(() => {
return “こっそり値を返すぞ”;
});

この仕様の背景には、「戻り値を返す既存の関数(例:`Array.prototype.push` など、数値を返す関数)を、戻り値を無視するコールバック(例:`forEach` の引数)として安全に扱いたい」というJavaScriptの柔軟性を救うための歴史的経緯がある。

しかし、厳密な型安全性を求めるモダンなフロントエンド開発においては、この「緩さ」が予期せぬバグの温床となる。特に非同期処理やイベントハンドラの設計では致命的だ。

—

2. 実務で起きる事故:非同期処理と `Promise` の罠

フロントエンドの現場で最も多い事故が、非同期関数の戻り値の取り扱いだ。

以下のコードを見てほしい。APIクライアントからデータを送信し、その完了をハンドリングするコンポーネントのコードだ。

// ❌ アンチパターン:非同期ハンドラの戻り値の型が曖昧
async function handleSubmit(data: FormData): Promise {
await api.post(“/save”, data);
// うっかり return を書き忘れた、あるいは将来の改修で値を返すように変更されたとする
return “saved_successfully” as any;
}

もし、この `handleSubmit` を受け取る親コンポーネントやイベントリスナーが、以下のように実装されていたらどうなるか。

// 親コンポーネントのボタンクリックハンドラ
button.addEventListener(“click”, async () => {
// Promise なので、戻り値は使わないはず…
await handleSubmit(formData);
setLoading(false);
});

一見問題なさそうに見えるが、もし `handleSubmit` が誤って(あるいはリファクタリングで)データを返すようになったとき、それを呼び出す側が `void` を期待していると、型システムはそれを検知できず、サイレントバグ(意図しないデータの隠蔽や、誤ったPromiseチェーンの構築)を引き起こす。

—

3. 【実践】バグを根絶する堅牢な設計パターン

では、この言語仕様の「緩さ」をねじ伏せ、プロダクションコードの堅牢性を担保するにはどうすればよいか。
答えは簡単だ。「戻り値を許容したくない関数コンテキストでは、厳格な型制約を強制する」ことだ。

以下に、実務でそのまま使える、保守性の高い美しいプロダクションコードのパターンを提示する。

パターンA: コールバックの戻り値を完全に捨てさせる (`unknown` や `never` の活用ではない、明示的な設計)

もし、コールバック関数に「絶対に値を返させたくない(戻り値を利用する余地を与えたくない)」場合は、関数型そのものを工夫するのではなく、呼び出し側での受け取り方を制限する、あるいはTypeScript 5.0以降で導入された言語機能を活用する。

しかし、最も確実なのは、TypeScriptの `exactOptionalPropertyTypes` や、戻り値の代入を厳格に制限するラッパー関数を用意することだ。

/

  • 渡されたコールバックが「確実に何も返さないこと」を強制し、
  • さらにその戻り値を絶対に外に漏らさない高階関数(セーフランナー)

/
function executeSafely(callback: () => void): void {
// コールバックの戻り値を絶対に変数にバインドさせない構造にする
callback();
}

// — 使用例 —

// OKな例
executeSafely(() => {
console.log(“サイドエフェクトのみを実行”);
});

// コンパイルエラーにしたい?
// 実は TypeScript の仕様上、() => void を受け取る関数に文法的に値を返す関数を渡すことは
// 「呼び出し側での戻り値の無視」を許可しているため許容される。
// では、完全に値を返すことをコンパイルエラーにするにはどうするか?

パターンB: 戻り値の型に `undefined` を強制し、かつ `return` 文を禁止するアプローチ

戻り値を `void` にするのではなく、「戻り値の型を記述しない(推論に頼る)」または「厳密に型安全なアサーションを入れる」。
いや、もっとモダンで強力なアプローチがある。それが `noImplicitReturns` の有効化だ。

`tsconfig.json` の設定を見直そう。

{
“compilerOptions”: {
“strict”: true,
“noImplicitReturns”: true, // すべてのコードパスで戻り値が明示されていない場合にエラーにする
“noUncheckedIndexedAccess”: true
}
}

このフラグを立てると、関数内で「値を返すパス」と「返さないパス」が混在している場合にコンパイルエラーになる。しかし、`void` を返す関数であっても `return;`(値なし)を書くことが強制されるため、うっかり値を返してしまう事故を防げる。

—

4. テクニカルリードからの最終提言:コードレビューの視点

関数の戻り値に `void` を定義するということは、単に「何も返さない」という宣言ではない。
「この関数が発火する副作用(Side Effect)以外のいかなる出力も、呼び出し元は依存してはならない」という、設計上の強い契約(Contract)なのだ。

コードレビューの際、以下のポイントを必ずチェックしてほしい。

1. `void` を返す関数の中で、意味のある値が `return` されていないか?
2. 非同期関数(`Promise`)のハンドラにおいて、意図せず値を返して型が緩くなっていないか?
3. コールバック関数を受け取る設計において、そのコールバックが「純粋な副作用」として機能しているか?

TypeScriptの型システムは強力だが、言語の仕様上の「寛容さ」がバグを生む隙間を作ることもある。その隙間を埋めるのが、我々エンジニアのアーキテクチャ設計だ。

型に寄りかかりすぎず、しかし型の裏側の挙動までを掌握した上で、美しく堅牢なコードを書き上げてくれ。健闘を祈る。

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