こんにちは!TypeScriptの型システムを旅する開発者の皆さん、調子はいかがですか?
「関数を引数に取る関数(高階関数)」を作ったとき、コールバック関数の引数に `any` がついてしまったり、自分でわざわざ型を書き直したりしてモヤモヤしたことはありませんか?
> 「親の関数のジェネリクスから、子のコールバック関数の引数の型を自動的に推論させたい!」
ここを綺麗にクリアできるようになると、TypeScriptの型推論の仕組み(底力)がグッと見えてきて、コードを書くのが一気に楽しくなりますよ。
今回は、初学者の方でも「なるほど!」と腑に落ちるように、優しく丁寧にその極意を解説していきますね。ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ!
—
1. よくあるモヤモヤ:「あれ、型推論してくれない…?」
まずは、私たちがよくやりがちな「惜しいコード」を見てみましょう。
例えば、データを処理して、その結果を加工するような高階関数を作りたいとします。
// データを処理する高階関数を作りたい!
function processer
// 何らかの処理…
callback(data);
}
// 使ってみる
processer(42, (value) => {
// ここで value は number として推論されてほしい…!
console.log(value.toFixed(2));
});
このコード、実は運良くうまく動きますよね。`data` に `42` (`number`) を渡しているので、コンパイラは `T = number` と推論し、`callback` の引数 `arg` も自動的に `number` になってくれます。
ですが、「複数の引数が絡み合う複雑な関数」や、「引数に渡す関数(コールバック)の戻り値や引数の関係性を段階的に確定させたい場合(カリー化など)」になると、途端に型推論が崩壊し始めます。
—
2. 推論が崩壊する壁:依存関係のあるジェネリクス
次のような、「ある関数の結果を、次の関数の引数にする」ようなパイプライン処理やカリー化(関数を段階的に呼び出す手法)を考えてみましょう。
// 引数に渡す関数の「引数の型」を、親のジェネリクスから推論させたい
function createValidator
return function(validatorFn: (arg: T) => boolean) {
return validatorFn(initialValue);
};
}
これ自体はうまく動きます。ですが、実務では「バリデーターに通した後の型に変化させたい」「複数の関数をチェインさせたい」という要望が出てきます。
ここで初心者の多くが、次のような文法エラーや「推論の放棄(`any`堕ち)」に直面します。
// ❌ 陥りがちな罠:型を明示しないと推論が途切れるケース
function pipe
return transform(arg);
}
// 使ってみる
const result = pipe(“hello”, (val) => {
// val は string と推論される(ここはOK)
return val.length;
});
// result の型は number になる(ここもOK)
「あれ?これなら動くじゃん!」と思いましたか?
素晴らしい着眼点です。しかし、この `pipe` を「関数を返す形(カリー化)」に書き換えた途端、TypeScriptのコンパイラは迷子になり始めます。
—
3. 解決の鍵:ジェネリクスの「段階的推論(カリー化テクニック)」
関数を返す高階関数において、型推論を正確に機能させるための黄金律、それは「型パラメータをどの関数に配置するか」です。
TypeScriptのコンパイラは、関数が呼び出された瞬間にジェネリクスの型を推論します。そのため、すべての型を一番外側の関数にまとめてしまうと、内側のコールバック関数の型がうまく連動しなくなるのです。
実用的なカリー化の例を見てみましょう。
// 🎯 正しいアプローチ:外側と内側でジェネリクスを分ける(あるいは適切に伝播させる)
// 1. まずデータを受け取る関数
function withLogger
// 2. 次に、そのデータを受け取って加工するコールバックを受け取る関数を返す
return function process
console.log(“Before process:”, initialData);
const result = action(initialData);
console.log(“After process:”, result);
return result;
};
}
このコードの何がスゴいのか?(型評価の裏側)
1. `withLogger(100)` と呼び出した瞬間、TypeScriptは `T = number` と確定させます。
2. 返された `process` 関数は、`T` がすでに `number` であることを知っています。
3. その結果、引数に取る `action` 関数の引数 `data` は、強制的に `number` 型として推論されるようになります。
4. さらに、`action` の戻り値の型 `R` も自動的に推論され、最終的な `process` の戻り値の型になります。
実際に使ってみましょう。
// 実際に使ってみる
const finalResult = withLogger(100500)( (num) => {
// num は自信を持って number 型として扱える!
// 万が一 num.toUpperCase() などと書くと、即座にコンパイルエラーが教えてくれる。
return `金額: ¥${num.toLocaleString()}`;
});
// finalResult の型は自動的に string に推論されている!
console.log(finalResult);
コンソールにはこのように出力され、型も完璧に守られます。
Before process: 100500
After process: 金額: ¥100,500
金額: ¥100,500
—
4. 陥りやすい文法エラーと注意点
ここで、開発現場でよくある失敗パターンと、その対策を押さえておきましょう。
⚠️ 失敗パターン1:不必要な型注釈(Type Annotation)を自分で書いてしまう
親切心のつもりで、コールバック側に自分で型を書いてしまうと、かえって推論の邪魔をしたり、意図しない型エラーを招くことがあります。
// ❌ 良くない例:自分で型を縛ってしまう
withLogger(100)((num: string) => { // エラー! number なのに string を受け取ろうとした
return num;
});
対策: TypeScriptの強力な推論を信じて、コールバックの引数の型は省略(お任せ)しましょう。コンパイラが親のジェネリクスから型を自動で流し込んでくれます。
⚠️ 失敗パターン2:ジェネリクスのスコープを混同する
「どこで型が決定されるか」を意識していないと、型が `unknown` や `any` に落ちてしまいます。関数を返すときは、「どのタイミングで型が確定すべきか」を意識して、関数ごとに `
—
まとめ
いかがだったでしょうか?
- 高階関数やカリー化において、コールバックの引数型を推論させるには「ジェネリクスの配置場所(スコープ)」が命。
- すべての型を一度に解決しようとせず、関数が呼ばれるステップごとに型を伝播させる。
- コンパイラの推論能力を信じて、無駄な型注釈を書かずに任せる。
このポイントさえ押さえれば、複雑なカスタムHooksや、高度なユーティリティ関数、関数型のライブラリ設計でも迷うことがなくなります。
型システムはあなたを縛る鎖ではなく、あなたの背中を押してくれる最強の相棒です。ぜひ今日のコードから試してみてくださいね!