こんにちは!TypeScriptの世界へようこそ。フルスタックアーキテクトの先輩として、今日から君をさらに一段上のステージへ導く特別なトピックを用意したよ。
今回は、関数型における「`void`」と「`undefined`」の戻り値の差異と、それが呼び出し元に与える影響について深く掘り下げていこう。
「え、関数から何も返さないときは `void` を使うんでしょ? `undefined` と何が違うの?」
「あれ? 戻り値が `void` のはずのコールバック関数の中で、うっかり `return 42;` って書いちゃったのに、なぜかTypeScriptのコンパイラが怒らないんだけど……バグ?」
そんな疑問を持ったことはないかな?
ここをクリアすれば、TypeScriptの型システムの本質と「なぜそのような仕様になっているのか」という言語デザインの哲学が手に取るようにわかるようになるよ。さあ、一緒に紐解いていこう!
—
1. そもそも `void` と `undefined` って何が違うの?
まずは基本の「おさらい」から。JavaScriptのランタイム(実行環境)の世界には、実は `void` という値は存在しない。存在するの実行時における `undefined` だけだよね。
でも、TypeScriptの型システムの世界では、この2つは明確に区別されている。
- `undefined`: 「値が存在しない」という具体的な値の型。JavaScriptの `undefined` というプリミティブ値そのものを表す。
- `void`: 「この関数は戻り値を意図して返しませんよ」という関数の契約(宣言)を表す型。
イメージ図で表すと、こんな感じかな。
【JavaScriptの世界(実行時)】
何も返さない関数 ──> 実体としての値は常に 「undefined」
【TypeScriptの世界(型システム)】
undefined 型 ──> 「undefined という値を返す」ことが厳格に決まっている
void 型 ──> 「何を返しても(あるいは何も返さなくても)無視するよ」という寛大な許可証
言葉だけだと少し抽象的だから、具体的なコードでその挙動の違いを見てみよう。
—
2. 戻り値の型による振る舞いの違い
まずは、戻り値に `undefined` を明示している関数と、`void` を使っている関数の違いを見てみるよ。
// 1. 戻り値が 「undefined」 の関数
function returnUndefined(): undefined {
// 正しい:undefined を返さなければならない
return undefined;
// ❌ エラー:Type ‘number’ is not assignable to type ‘undefined’.
// return 42;
}
// 2. 戻り値が 「void」 の関数
function returnVoid(): void {
// 何も返さなくてもOK
return;
// または単に何もしなくてもOK
// ❓ え? これ、エラーにならないの……?
return 42 as any; // (極端な例だけど、void型は値を返すこと自体は許容する文脈がある)
}
「あれ? `void` なのに値を返せてしまうことがあるの?」と思った鋭い君。そここそが今回の最大のテーマなんだ。次はその謎に迫るよ。
—
3. 【核心】なぜコールバックの `void` は値を返してもエラーにならないのか?
TypeScriptで最も混乱しやすいポイントがここ。
例えば、配列の `forEach` メソッドを使うときを想像してほしい。`forEach` に渡すコールバック関数の戻り値の型は、TypeScriptの標準ライブラリ(lib.es5.d.tsなど)で `void` と定義されている。
でも、こんなコードを書いたことはないかな?
const numbers = [1, 2, 3];
const results: number[] = [];
// forEach のコールバックの戻り値は void のはず…
numbers.forEach((num) => {
// あえて値を返してみる(Array.prototype.push は追加後の配列の長さを返す)
return results.push(num 2);
});
console.log(results); // [2, 4, 6]
不思議なことに、このコードは一切エラーを出さずにコンパイルが通り、正常に実行される。
「`void` なんだから、戻り値を返しちゃいけないんじゃないの?」って思うよね。なぜTypeScriptはこのコードを許容するのだろう?
理由:TypeScriptの「コールバックにおける `void`」の寛大な設計
これはバグではなく、TypeScriptチームが意図して設計した言語仕様なんだ。
JavaScriptやTypeScriptの開発では、既存のライブラリや関数(例えば `Array.prototype.map` や独自のユーティリティ関数など)のコールバックとして、たまたま「戻り値を返す関数」を渡したい場面が非常によくある。
もし、コールバックの型が `void` だという理由だけで、「中で `return` 文を書くことすら厳格に禁止する」仕様にしてしまったらどうなるだろう?
// もし厳格だったら…
numbers.forEach((num) => {
// push は数値を返すので、voidを期待する場所ではエラーになる!
return results.push(num 2); // ❌ 怒られてしまう
});
これだと、既存の便利な関数(値を返す関数など)をコールバックにそのまま渡せなくなってしまい、開発の生産性が著しく落ちてしまうよね。
そのため、TypeScriptの型システムでは、「ある関数型(コールバック等)が `void` の戻り値を期待している場所には、どんな戻り値を持つ関数を代入しても構わない(返された値は単に捨てられる)」というルールを採用しているんだ。
これを型安全の言葉で言い換えると、「`void` を期待する文脈において、任意の戻り値を持つ関数はサブタイプとして扱われる」ということになる。
—
4. ただし、「直接呼び出す側」では話が違う!
ここで注意してほしい大切なポイントがある。
「`void` の関数なら何を返してもいいんでしょ?」と勘違いして、自分で定義した `void` 関数を直接呼び出してその戻り値を使おうとすると、しっかりエラーになるんだ。
// 戻り値が void の関数を定義
function logMessage(message: string): void {
console.log(message);
// 特に何も返さない(実際には undefined が返る)
}
// 自分で定義した関数を呼び出す場合
const result = logMessage(“こんにちは”);
// 💡 このとき、result の型は 「void」 になる
// ❌ エラー:This expression is not callable. 又は、void型なのでプロパティにアクセスできない等
// もし、result を何かの値として使おうとすると、TypeScriptはしっかり止めてくれる!
// if (result.length > 0) { … }
ここが重要な違いだよ。
- コールバックとして渡すとき: 相手が `void` を求めているなら、こちらはどんな値を返しても(TypeScriptがそれを無視してくれるので)許される。
- 直接戻り値を受け取る変数: `void` 型の変数を `string` や `number` の代わりに使うことはできない(値が存在しない、あるいは無視すべきものとして扱われるため)。
—
5. 厳密に値を返させたくない場合のテクニック
もし、自分が作成するライブラリや関数で、「コールバック関数には絶対に何も返してほしくない(バグを防ぎたい)」という厳格な設計にしたい場合はどうすればいいだろう?
実は、通常の `void` を使うだけだと、先ほど説明した「寛大なルール」が適用されてしまう。その場合は、以下のように型を工夫する必要がある。
// ❌ 寛大なパターン(利用者がうっかり値を返せてしまう)
function processItems(callback: (item: string) => void) {
// …
}
// ⭕️ 厳格なパターン(値を返すことを型レベルで完全に禁止する)
function processItemsStrict(callback: (item: string) => undefined) {
// …
}
// 使用例
processItemsStrict((item) => {
// return 123;
// ❌ Type ‘number’ is not assignable to type ‘undefined’.
// ここではしっかりとコンパイルエラーになり、開発者のミスを防げる!
});
このように、どうしても「戻り値の不在」を厳密に強制させたい場合は、`void` ではなく `undefined` をあえて採用するというテクニックが使えるんだ。
—
まとめ:ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ!
今回は、関数型における `void` と `undefined` の違い、そしてコールバックにおける `void` の寛大な型評価の裏側について解説したよ。
1. `undefined` は「値としての `undefined`」を厳格に要求する。
2. `void` は「戻り値は利用しない(無視する)」という意図を表す。
3. コールバック関数において、受け取り側が `void` を期待している場合、渡す側がどんな値を返してもTypeScriptはエラーにしない(開発の利便性を高めるための言語仕様)。
4. ただし、自分で呼び出した関数の結果を `void` 型として受け取った場合、それを別の値として扱うことはできない。
この仕組みを理解しておくと、サードパーティ製ライブラリの複雑な型定義を見たときにも「なぜここでこの型エラーが出るのか」「なぜこの書き方が許されるのか」がスッと頭に入ってくるようになるよ。
TypeScriptの型システムは、私たち開発者の邪魔をするためではなく、「JavaScriptの柔軟性を活かしつつ、うっかりミスを防ぐため」に絶妙なバランスで設計されているんだ。
この本質が腑に落ちたなら、もう君のTypeScriptの基礎力は本物だ。自信を持って次のステップへ進もう!