こんにちは。TypeScriptの世界へようこそ。
今日は、多くの開発者が「わかったつもり」で通り過ぎてしまいがちな、しかしTypeScriptの型システムの本質がギュッと詰まった「void」という戻り値の真実についてお話ししましょう。
「`void`は値を返さないという意味ですよね?」
半分正解ですが、TypeScriptの深淵においては、その理解だけではいつか足元をすくわれてしまいます。
この記事を読み終える頃には、あなたは`void`を単なる「空っぽの印」ではなく、「呼び出し側との契約」として正しく使いこなせるようになっているはずです。ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ。
—
1. そもそも「void」とは何を指しているのか?
C言語やJavaを学んだ方にとって、`void`は「何も返さない」ことを強制する厳格な命令に見えるかもしれません。しかし、TypeScript(およびJavaScript)における`void`の振る舞いはもう少し柔軟で、知的です。
TypeScriptにおける`void`の真の意味は、「その戻り値は利用されない(無視される)ことを期待している」という意思表示にあります。
基本の形
まずは、最も一般的な書き方を見てみましょう。
// 基本的なvoidの使い方
function logMessage(message: string): void {
console.log(`Log: ${message}`);
// return文がない、あるいは単に return; と書く場合
}
const result = logMessage(“Hello”);
// result の型は void。実体は undefined です。
ここまでは簡単ですね。では、ここからが本題です。
—
2. 【衝撃】voidなのに値を返せてしまう不思議
TypeScriptには、初心者の方が驚くような挙動があります。それは「戻り値の型がvoidとして定義されているのに、実際には値を返している関数を代入できてしまう」という仕様です。
以下のコードを見てください。
// 1. voidを返すと定義された関数の型
type VoidCallback = () => void;
// 2. 実際には「number」を返す関数
const sower = (): number => {
return 42;
};
// 3. 【注目】これがエラーにならない!
const execute: VoidCallback = sower;
// 4. 実行すると、値はしっかり返ってきている
const value = execute();
console.log(value); // 42 と表示される。でも、TypeScript上の型は void
「えっ、型エラーにならないの?」と思いましたよね。実は、これこそがTypeScriptの設計思想である「代入可能性(Assignability)」の妙技なのです。
なぜこの挙動が許されるのか?
この仕様の最大の理由は、JavaScriptの既存の書き方を壊さないためです。
一番分かりやすい例が、配列の `forEach` です。
const numbers = [1, 2, 3];
const resultList: number[] = [];
// Array.prototype.forEach のコールバックの期待される型は () => void です。
// しかし、pushメソッドは「新しい配列の長さ(number)」を返します。
numbers.forEach(num => resultList.push(num));
もし、`void`が「絶対に値を返してはいけない」という厳格なルールだったら、上記の `forEach` のコードはエラーになってしまいます(`push`が値を返してしまうため)。
いちいち `numbers.forEach(num => { resultList.push(num); return; });` と書くのは苦痛ですよね。だからTypeScriptは、「呼び出し側が『戻り値を使わない(void)』と言っているなら、関数が何を返そうが勝手だ」と容認してくれるのです。
—
3. 「関数宣言」と「関数型」で振る舞いが変わる罠
ここが非常に重要なポイントです。`void`の挙動は、どこに書くかで厳格さが変わります。
パターンA:関数宣言(厳格)
関数を直接定義するときに`void`を指定すると、値を返すと即座に怒られます。
// これはコンパイルエラー!
function literalVoid(): void {
return “Oops!”; // Error: 型 ‘string’ を型 ‘void’ に割り当てることはできません。
}
パターンB:関数型のリテラル(寛容)
先ほどの例のように、変数に対して「型」として`void`を割り当てた場合は、値を返すことが許容されます。
type Callback = () => void;
const looseVoid: Callback = () => “I’m free!”; // OK
この違いは、「自分で定義した関数の中でうっかり値を返してしまうミス」は防ぎつつ、「他の関数に渡すコールバックとしての互換性」は保つという、プロフェッショナルのための絶妙なバランス調整なのです。
—
4. 安全な設計のための指針
「自由なのはわかったけれど、どう使い分ければいいの?」という疑問に、アーキテクトの視点でお答えします。
① 本当に何も返さないなら `void`
基本はこれです。呼び出し側に「この関数の結果を利用しないでね」と伝える最もポピュラーな方法です。
② 「戻り値がないこと」を厳密に保証したいなら `undefined`
もし、その関数が「絶対に何も返してはいけない(またはundefinedのみを返すべき)」と強く制約したい場合は、`void`ではなく`undefined`を戻り値に指定します。
function strictUndefined(): undefined {
// return “something”; // これはエラーになる
return; // これならOK
}
③ 呼び出し元で値を無視することを徹底させる
`void`を返す関数から得た戻り値を、別の変数に代入して計算に使おうとすると、TypeScriptは警告を出してくれます。
function doSomething(): void { return; }
// const val = doSomething() + 10;
// Error: 演算子 ‘+’ を型 ‘void’ および ‘number’ に適用することはできません。
このように、`void`は「実行時の値を制限する」のではなく「コンパイル時の利用方法を制限する」ための強力なタグとして機能するのです。
—
5. まとめ:TypeScriptを掌握する一歩
今回のポイントを整理しましょう。
1. `void`の本質: 「戻り値がない」ことではなく、「戻り値が利用されない」という契約である。
2. 代入の柔軟性: 関数型として定義された `void` は、他の型を返す関数を受け入れることができる(`forEach`などの利便性のため)。
3. 定義の厳格性: 関数宣言で直接 `void` を指定した場合は、値を返すとエラーになる。
4. 設計の使い分け: ゆるやかな制限なら `void`、厳格に制限するなら `undefined`。
TypeScriptの型システムは、単にバグを見つけるための道具ではありません。「コードを通じて、他の開発者(あるいは未来の自分)とどんな対話をするか」を定義するための言語です。
「この関数は何か作業をするけれど、結果には期待しないでね」というメッセージを込めて `void` を使う。その意図が正しく伝わるコードは、とても美しく、メンテナンスしやすいものです。
ここまでの内容を理解できれば、関数の型定義の基礎はもうあなたのものです。自信を持ってコードを書いていきましょう。次は「never型」や「unknown型」といった、さらに深い型定義の冒険があなたを待っていますよ!
また次のレッスンでお会いしましょう。ハッピーコーディング!