【入門編】関数型における「void」と「undefined」の戻り値の差異と、呼び出し元への影響 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの型システムの世界へようこそ。
フルスタックアーキテクトの先輩として、今日から君をさらに一歩上のステージへ導く「とっておきの知見」を授けましょう。

今回は、関数における「`void`」と「`undefined`」の戻り値の決定的な違いについて深掘りします。「えっ、何も返さない関数なら、どっちでも一緒じゃないの?」なんて思っていませんか?

ここをクリアすれば、TypeScriptの型安全性の本質が見えてきます。一緒にしっかりとマスターしていきましょう!

—

1. そもそも `void` と `undefined` って何が違うの?

まずは、それぞれの基本的な意味をおさらいしておきますね。

  • `undefined`: 「値が存在する」という状態。ただし、その値が「未定義(何も代入されていない)」であることを表すJavaScriptのプリミティブ値そのものです。
  • `void`: 「戻り値は意図的に無視される(意味を持たない)」というTypeScript特有の型。実行時には `undefined` が返ることが多いですが、型システム的には「呼び出し元は、この関数の戻り値を使ってはいけない」という強い制約を意味します。

イメージ図で表すと、こんな感じです。

【 戻り値の型ごとの世界観 】

undefined型 ──> 「ここに『undefinedという値』が存在します。使ってもいいですよ」
void型 ──> 「ここは『何も見ちゃダメなブラックボックス』です。戻り値は捨てられます」

—

2. なぜ、戻り値が `void` の関数で `undefined` を返してはいけないのか?

さて、ここからが本題です。
例えば、「ログを出力するだけ」の関数を作るとします。何も値を返さないので、戻り値を `void` に設定しました。このとき、うっかり `return undefined;` と書いてしまうとどうなるでしょうか?

実際のコードを見てみましょう。

// 戻り値を「void」と宣言した関数
function logMessage(message: string): void {
console.log(`[LOG]: ${message}`);

// ❌ やってしまいがちなミス:明示的に undefined を返している
return undefined;
// 【コンパイルエラー!】
// 型 ‘undefined’ を型 ‘void’ に割り当てることはできません。
}

「えっ、`void` の実体って `undefined` なんでしょ? なぜエラーになるの?」と思いますよね。ここにはTypeScriptの「呼び出し元を守るための優しさ(型安全性の担保)」が隠されています。

TypeScriptがエラーにする理由:契約の違反を防ぐため

TypeScriptにおいて、関数の戻り値の型は「呼び出し元との契約(Contract)」です。

  • `void` の契約: 「この関数を呼んでも、便利な戻り値なんて何も返ってこないから、受け取って処理に使おうなんて思わないでね」
  • `undefined` の契約: 「この関数は、明確に `undefined` という値を返すので、もし必要ならチェックしてね」

もし、`void` と宣言された関数の中で `undefined` を返すことを許可してしまうと、呼び出し元で次のような危険なコードが書けてしまいます。

// 仮に void が undefined を返すことを許してしまった場合の世界線…
function doSomething(): void {
return undefined; // 許されたとする
}

// 呼び出し元
const result = doSomething();

// 「戻り値がある」と誤認して、さらにプロパティにアクセスしようとするバグを呼びかねない
// result.toString(); // 実行時に “Cannot read properties of undefined” でクラッシュ!

TypeScriptのコンパイラは、私たちがこうした「意図しないバグ」の沼にハマるのを未然に防ぐために、`void` 型の関数内で `undefined` を(明示的にせよ)返すことを厳しくブロックしてくれているのです。

—

3. コールバック関数を扱うときに見えやすくなる「罠」

この `void` と `undefined` の違いが最も牙をむく(そして初心者が一番ハマる)のが、コールバック関数を型定義する時です。

例えば、「何かを受け取って処理し、何も返さない関数」を引数に取る関数を作るとしましょう。

// 引数として「何も返さない関数(void)」を受け取る高階関数
function processUser(callback: (name: string) => void) {
// ユーザー処理…
callback(“Taro”);
}

// パターンA: 戻り値がない通常の関数を渡す(OK)
processUser((name) => {
console.log(name);
});

// パターンB: 意図せず undefined を返す関数を渡そうとする(エラーになる!)
processUser((name) => {
console.log(name);
return undefined; // ❌ ここで型エラー!
});

「え、でもArrayの `forEach` とかでは `undefined` 返しても怒られない気がするけど……?」と思った鋭いあなた、素晴らしい着眼点です!

実は、TypeScriptの型システムには 「`void` の亜種(サブタイプ)の許容」 という柔軟なルールがあります。

`void` 型が持つ特別な寛容さ

TypeScriptでは、ある関数が `void` を返すことを要求している(期待している)場合、実際に何を返そうが(あるいは何も返そうが)、それを無視して受け入れるという仕様があります。

// 戻り値が void を期待されている場所には…
const f: () => void = () => {
return “Hello”; // なんと、文字列を返してもエラーにならない!
};
えっ、さっきと言っていることが違うじゃん!と思われるかもしれません。整理しましょう。

1. 関数を「定義」するとき: `function foo(): void { return undefined; }` のように、関数自身の型注釈で `void` と書いた場合、明示的に `undefined` や他の値を `return` することは原則として禁止されます(バグを防ぐため)。
2. 関数を「代入・引数として渡す」とき: 呼び出し側が「別に何を返しても無視するよ(`void` で受けるよ)」と言っている場所には、`undefined` や何かしらの値を返す関数を渡すことができます。

この挙動は、JavaScriptの一般的なライブラリ(例えば `Array.prototype.forEach` などは、コールバックの戻り値を意図的に捨てます)と既存のコードベースをうまく共存させるための、TypeScriptの高度な設計思想に基づいています。

—

4. 安全な設計のためのベストプラクティス

現場でコードを書くとき、私たちはどう設計すべきでしょうか? 迷ったときは以下の原則を思い出してください。

① 本当に何も返さないなら、`return` 文自体を書かない

`void` を返す関数では、そもそも `return` キーワードを書く必要はありません。処理の最後まで到達すればそれで十分です。

// 模範的なコード
function saveToDatabase(data: object): void {
// データベース保存処理
console.log(“Saved!”, data);

// return は書かない!
}

② もし「何もない」という値を明示的に返したいなら、型に `undefined` を使う

もし呼び出し元に対して「この関数は正常終了した証として `undefined` を返すよ」と明確に契約したい場合は、`void` ではなく `undefined` を使いましょう。

// 戻り値として undefined を許容・期待する設計
function findUserById(id: number): string | undefined {
if (id === 1) {
return “Taro”;
}
// 見つからなかった場合は明確に undefined を返す
return undefined;
}

—

まとめ

いかがでしたでしょうか? 今回のポイントをギュッと凝縮して振り返ります。

  • `void` は「戻り値を使わせないための防壁(コンパイラへの誓約)」。
  • `undefined` は「実際に `undefined` という値が存在するという事実」。
  • 自分で定義する `void` 関数の中で `return undefined;` と書くのは、型安全性の観点からナンセンス(かつコンパイルエラーになる)。
  • コールバックなどで `void` が期待されている場所では、柔軟性のために値の返却が許容されるケースもあるが、基本は「何も返さない」のが最もクリーン。

ここをしっかりと理解できれば、TypeScriptの型がなぜそのエラーを出しているのか、コンパイラの意図が手に取るようにわかるようになります。

基礎から本質までを理解したあなたなら、もう怖くありません。明日からのコーディングをさらに楽しんでいきましょう!バッチリマスターできましたね!

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