【入門編】引数に渡す「関数型」の定義における「void」の柔軟な解釈 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの型システムの世界へようこそ。
日々フロントエンドからバックエンドまでコードを書いていると、「あれ? この型定義、なぜかエラーにならないぞ?」と不思議に思う瞬間に出会いませんか?

TypeScriptの型システムは、一見すると厳格ですが、実は「実用性のためにあえて少し寛容になっている仕様」がいくつか存在します。その代表格が、今回取り上げる「関数型における `void` の柔軟な解釈」です。

他の厳密なプログラミング言語(JavaやC#など)からやってきた開発者ほど、「えっ、何も返さないはずの場所に、値を返す関数を渡して怒られないの!?」と混乱しがちです。

ここをクリアすれば、TypeScriptのコールバック関数やイベント処理の型定義に対する解釈がグッと深まり、ライブラリの型定義を読むのも楽しくなりますよ。さあ、一緒にその仕組みを紐解いていきましょう!

—

1. そもそも `void` って何だっけ?

まずは基本のおさらいからですね。
TypeScriptにおける `void` は、「関数が意味のある戻り値を返さないこと」を表す型です。

例えば、コンソールにログを出力するだけの関数を考えてみましょう。

// 戻り値がない(厳密には undefined を返しているが、意味のある値ではない)関数
function logMessage(message: string): void {
console.log(`[LOG]: ${message}`);
}

この `logMessage` 関数の戻り値の型は `void` です。これは、「この関数を呼び出しても、受け取った側はその戻り値を使って何か処理をすることを期待しないでね」というコントラクト(契約)になります。

—

2. 本題:`void` を返す関数型に「値を返す関数」を渡せるという謎

さて、ここからが本題です。
あなたが共通のユーティリティ関数や、イベントリスナーを受け取る関数を作っているとします。引数として「何も返さない(`void` を返す)コールバック関数」を受け取る仕様にしました。

コードで書くとこんな感じです。

// コールバックとして「引数なし、戻り値voidの関数」を受け取るロガー実行機
function executeTask(task: () => void): void {
console.log(“タスクを開始します…”);
task(); // コールバックを実行
console.log(“タスクが終了しました。”);
}

この `executeTask` に対して、もしあなたが次のような「文字列を返す関数」を渡したらどうなるでしょうか?

// 文字列を返す関数
function getUsername(): string {
return “Taro TypeScript”;
}

// さて、これを渡すとどうなる…?
executeTask(getUsername);

他の言語の感覚だと、「`() => void` なのに、`() => string` を渡すなんて型ミスマッチだ!」と赤色の波線(コンパイルエラー)が出そうなものですよね。

しかし、TypeScriptでこれをコンパイル・実行しても……エラーにはなりません。何事もなく正常に動きます。

[出力結果]
タスクを開始します…
Taro TypeScript
タスクが終了しました。

「えっ、`void` って言ってなかったっけ!? なぜ `string` を返す関数を渡しても怒られないの?」と驚きますよね。この振る舞いこそが、TypeScriptの型システムにおける実用的な寛容さ(サブタイピングのルール)なのです。

—

3. なぜこの仕様になっているのか?(TypeScriptの優しさ)

なぜTypeScriptはこのコードを許容するのでしょうか?
それは、「呼び出し側(受け取る側)の都合」に焦点を当てて考えてみると、非常に理にかなっていることが分かります。

`executeTask` 関数側の視点になってみましょう。

function executeTask(task: () => void): void {
// executeTask側は、task() の戻り値を受け取る気がない(捨てている)
task();
}

`executeTask` は、`task()` を実行しているだけで、その返ってきた値(もしあれば)を変数に代入したりしていませんよね。ただ「実行して、終わり」です。

ということは、渡されたコールバック関数が、たまたま `string` や `number` やオブジェクトといった「余分な値」を返してきたとしても、`executeTask` 側がそれを無視してさえいれば、実行時に何の問題も起きないのです。

もしこれが厳格にエラーになってしまうとどうなるでしょう?
世の中にある「値を返す便利な既存の関数」を、`void` を期待するイベントハンドラやコールバックに渡すたびに、わざわざラップ(無名関数で包み込んで戻り値を捨てる処理)を書かなくてはならなくなります。

// もし厳格だったら、わざわざこう書く必要が出てしまう
executeTask(() => {
getUsername(); // 戻り値を捨てている
});

これは非常に面倒ですし、コードが冗長になります。そのためTypeScriptは、「`void` を返す関数型には、どんな値を返す関数を渡しても安全(戻り値は単に捨てられるだけなので無害)」というルールを採用しているのです。

—

4. 逆に注意!「代入」のときとはルールが違う?

ここで、「あれ、でも変数に代入するときは厳しかった気がするぞ?」と勘の鋭い方は気づいたかもしれません。その通りです!TypeScriptの面白い(そして少し混乱しやすい)ところは、文脈によってチェックの厳しさが変わる点です。

関数の「引数」として渡すときは今回の `void` の柔軟性が働きますが、関数をそのまま別の変数に「代入」しようとすると、話が変わることがあります。

// 変数の型定義
let myFunc: () => void;

// これはOK(void型の変数に、stringを返す関数を代入してもエラーにならないケースもある)
// ただし、strictNullChecks や設定、位置によっては厳密にチェックされることもあるため注意が必要

特に注意したいのは、「戻り値の型が `void` である関数から、実際に値を明示的に返そうとする」場合です。

// NG例:自分自身が「voidを返す」と宣言しているのに、値を返そうとするとエラーになる
const badFunction: () => void = () => {
return “Error!”; // ❌ Type ‘string’ is not assignable to type ‘void’.
};

ここを混同しやすいので整理しておきましょう。

1. 関数を渡す側(コールバック): 「何も返さないでね(`void`)」と言われている場所に、「何かを返す関数」を渡すのは OK(受け取る側が戻り値を無視するため)。
2. 関数を定義する側(本体): 「何も返さない(`void`)」と自分で宣言した関数の中で、`return 123;` のように値を返そうとするのは NG。

—

5. 現場で役立つ安全な運用法とまとめ

この `void` の柔軟性は非常に便利ですが、実務でコードを書くときは1点だけ注意が必要です。それは、「うっかり意図しない戻り値を受け取るコードを書いてしまわないこと」です。

例えば、コールバックの戻り値を使って何か条件分岐をしたい場合、型定義が `() => void` になっていると、TypeScriptは「そこには値がない」と判断するため、以下のようなコードを書くとコンパイラに止められます。

function processAndCheck(callback: () => void) {
const result = callback();

// ⚠️ callback の型が void なので、result の型は void になり、
// もしここに値を期待する処理を書いているならバグの元になる!
if (result) { … }
}

もし、コールバック関数が返す値を使って何かをしたいのであれば、最初から `void` ではなく具体的な型(`string` や `boolean` など)を型パラメータや戻り値に指定するべきです。

// 正しく戻り値を活用したい場合の定義
function processAndCheckStrict(callback: () => T): T {
return callback();
}

今日のまとめ

  • TypeScriptの `void` を返す関数型には、実際に値を返す関数を渡してもエラーにならない(呼び出し側が値を捨ててくれるため安全)。
  • この仕様のおかげで、既存の関数やユーティリティをコールバックとしてスムーズに渡すことができ、冗長なラッパー関数を書かずに済む。
  • ただし、自分が定義する関数本体の中で `void` なのに値を返そうとすると、それはしっかりとエラーになるので注意。

ここをクリアできれば、TypeScriptの型の「寛容さと厳格さの絶妙なバランス」が手に取るようにわかるようになります。イベントリスナーや非同期処理のコールバックを書くときにも、もう迷うことはありませんね!

それでは、快適なTypeScriptライフを!バッチリマスターしていきましょう!

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