コードレビューをしていて、最も頻繁に、そして見落とされがちに見るアンチパターンの一つがこれだ。
// ❌ よくある「なんとなく」書かれた危険なコード
const handleClick = (): void => {
if (isSubmitting) return undefined; // ここでコンパイルエラーにならない、あるいは意図せずundefinedを返す
doSomething();
};
一見すると何気ないこのコード、実はTypeScriptの型システムと実行時の安全性を揺るがす「地雷」が埋まっている。
今回は、関数型における `void` と `undefined` の決定的な差異を言語仕様の深部から紐解き、なぜ `void` を返す関数で `undefined` を扱ってはいけないのか、そしてプロフェッショナルな現場で通用する堅牢な設計とは何かを徹底的に解説しよう。
—
1. 型の仕様解剖:`void` は「値がない」ではなく「値を無視する」である
まず、TypeScript(およびJavaScript)における `void` の定義を正しくアップデートしてほしい。
多くの開発者は、`void` を「C言語のような何も返さない(`undefined` を返す)」という意味だと誤解している。しかし、TypeScriptの型システムにおいて、`void` は「この関数が何を返そうとも、呼び出し元はその戻り値を一切利用してはならない」という制約(コントラクト)である。
ここで、TypeScriptのコンパイラが持つ「寛容さ(Bivariance / Assignability)」が生む一つの特徴を見てみよう。
// 戻り値が undefined の関数
const returnsUndefined = (): undefined => {
return undefined;
};
// 戻り値が void の変数に代入してみる
const fn: void = returnsUndefined(); // ⚠️ エラーにならない!
なぜエラーにならないのか?
TypeScriptでは、`void` 型を持つコンテキスト(変数やパラメータ)に対して、任意の型を返す関数を割り当てることが許可されている。これは、コールバック関数(例:`Array.prototype.forEach` やイベントハンドラ)に、何かを返す関数を安全に渡すための仕様上の慈悲である。
しかし、この仕様の逆、すなわち `void` を期待する場所に `undefined` を明示的に持ち込むこと は、コードの意図を曖昧にし、将来ののリファクタリングで致命的なバグを生む。
—
2. なぜ `void` の関数で `undefined` を返してはいけないのか?
理由は主に3つある。テクニカルリードとして、レビュー時には厳しく指摘するポイントだ。
① 呼び出し元との「契約(Contract)」の破壊
関数の戻り値に `void` を指定するということは、「この関数を呼び出しても、戻り値を使った条件分岐や変数代入をしてはならない」という開発者間の強い意思表示だ。
そこに `return undefined;` と書く行為は、「ひょっとしたら戻り値に意味があるかもしれない」というノイズを呼び出し元に与え、コードの可読性を著しく下げる。
② 高階関数やコールバックにおける型推論の崩壊
特にReactのイベントハンドラや、非同期のパイプライン処理において、この差異は顕著に現れる。
// 型定義:引数に「何も返さない(void)」関数を受け取るカスタムフック
function useDebounceEffect(callback: () => void, delay: number) {
// …内部実装
}
// ❌ 悪例:中で undefined を返してしまう関数を渡す
useDebounceEffect(() => {
if (!isValid) return undefined; // コンパイラはこれを許容してしまうことがある
executeApiCall();
}, 300);
もし、このコールバックの戻り値を将来的に別のユーティリティでラップしたり、厳密な関数型プログラミングのパターン(Monad的な処理など)に組み込んだりした際、`undefined` が混入していることで予期せぬランタイムバグを引き起こす。
③ ジェネリクスや条件付き型(Conditional Types)でのミスマッチ
メタなライブラリコードや高度な型推論を書く際、`void` と `undefined` は完全に別物として扱われる。`void` を期待するジェネリクスに `undefined` を渡すと、型推論のエンジンが意図しない推論を行い、型安全性の網からすり抜ける原因になる。
—
3. 実践:プロダクションコードにおける堅牢な設計パターン
では、実際のフロントエンド開発(React、APIクライアント、イベント処理)において、どのようにコードを書くべきか。
「早期リターン」を多用する現場で、美しくかつ型安全なパターンを見ていこう。
パターンA:早期リターン時は「値なしの return」を使う
もし条件分岐によって処理を中断したい場合は、値を書かずに単に `return;` と書くべきだ。これにより、この関数が何も返さない(`void`)ことが視覚的にも型としても明確になる。
type UserFormProps = {
onSubmit: (data: FormData) => void;
};
export const UserProfileCard: React.FC
const handleSubmit = (e: React.FormEvent
e.preventDefault();
const formData = new FormData(e.currentTarget);
// 🔴 悪い例: return undefined; と書かない
// if (!formData.get(‘username’)) return undefined;
// 🟢 良い例: 値を伴わない return で早期リターンする
if (!formData.get(‘username’)) {
console.warn(‘Username is required.’);
return;
}
onSubmit(formData);
};
return
;
};
パターンB:値の有無を返す関数と、副作用を起こす関数を完全に分離する
「データをチェックして存在しなければ処理を止めたい(かつその理由をハンドリングしたい)」という要件がある場合、それは一つの関数に詰め込むべきではない。「値を取得・検証する関数」と「副作用を実行する関数」に責任を分離する。
// 1. 純粋にデータを検証し、Result型や undefined を返す関数
const parseInputData = (raw: unknown): FormData | undefined => {
if (!isValidData(raw)) return undefined;
return transformToFormData(raw);
};
// 2. 副作用(void)を実行する関数
const processSubmission = (data: FormData): void => {
apiClient.post(‘/users’, data);
};
// 3. 呼び出し側での調停
const handleAction = (raw: unknown) => {
const data = parseInputData(raw);
if (!data) {
// ここでは undefined であることを明示的にハンドリングして良い
showNotification(‘Invalid input’);
return;
}
// ここからは安全に void 関数へ渡す
processSubmission(data);
};
—
4. チーフアーキテクトからの提言
TypeScriptの型システムは、単なる「バグを防ぐためのガードレール」ではない。それはコードの意図を正確に伝えるためのドキュメントであり、チーム全体のスケーラビリティを担保する言語契約である。
- `void` が指定された関数では、絶対に `undefined` を `return` しない。値を返さずに抜けるときは、単なる `return;` を使う。
- 「値を返すかもしれないし、返さないかもしれない」という曖昧な設計は避け、関数を分離する。
この小さな意識の積み重ねが、何万行にも及ぶ大規模アプリケーションのコードベースを美しく、そして強靭に保ち続ける秘訣だ。
次のコードレビューでは、チームメンバーの `return undefined;` を見逃さないでほしい。