こんにちは!TypeScriptの型システムの世界へようこそ。
日々フロントエンドからバックエンドまでバリバリコードを書いていると、「あれ? この関数、本当はエラーになってほしいのに、なぜかスルーされてバグった……」なんて経験はありませんか?
TypeScriptの型システムはとても賢いですが、その優しさ(柔軟性)ゆえに、一歩間違えると「型安全のほころび」を生んでしまうことがあります。
今回は、関数を別の関数に渡すとき(高階関数やコールバックなど)に絶対に知っておくべき「関数の引数の共変性と反変性(特に、引数における型安全の罠)」について、優しく、そして本質的に解説していきますね。
ここをクリアすれば、あなたのTypeScript力は一段と「本物のプロフェッショナル」に近づきますよ!
—
1. そもそもTypeScriptの型合わせってどうなってるの?(おさらい)
TypeScriptは「構造的部分型(Structural Subtyping)」という仕組みを採用しています。「名前が違っても、中身の構造が同じ(または要件を満たしている)ならOK!」という考え方ですね。
例えば、次のようなケースを考えてみましょう。
// 動物の基本型
type Animal = {
name: string;
};
// 犬型(Animalのプロパティをすべて持っている+追加のプロパティがある)
type Dog = {
name: string;
bark: () => void;
};
// Animalを要求する関数
function callAnimalName(animal: Animal) {
console.log(animal.name);
}
const myDog: Dog = {
name: “ポチ”,
bark: () => console.log(“ワン!”)
};
// これは当然OK! DogはAnimalの要件を満たしているから代入できる
callAnimalName(myDog);
「犬は動物の一種だから、動物を欲しがっている場所に犬を渡せる」。これは直感的にも分かりやすいですよね。これをオブジェクトの代入における「共変(Covariant)」な関係と言います。
……ですが! 「関数を引数に取る関数」を扱い始めた途端、この直感が裏切られる事件が起きるのです。
—
2. 罠の始まり:関数を引数に渡すときにおこる「逆転現象」
次のようなシチュエーションを想像してください。
あなたは「犬専門のトレーナー」に指示を出すシステムを作っています。
- `DogHandler` 型:犬(Dog)を受け取って何かをする関数
- `AnimalHandler` 型:すべての動物(Animal)を受け取って何かをする関数
ここで、「動物全般を扱える優秀なトレーナー(AnimalHandler)」を求めている場所に、「犬しか扱えない特化型トレーナー(DogHandler)」を代入しようとしたらどうなるでしょうか?
頭の中で考えてみてください。「動物全般OK」なところに「犬専用」を放り込むんだから、なんだか危険な気がしますよね。でも、TypeScriptのコンパイラはこれをどう評価するでしょうか?
実際のコードで見てみましょう。
type Animal = { name: string };
type Dog = { name: string; bark: () => void };
// 犬専用のハンドラー
type DogHandler = (dog: Dog) => void;
// すべての動物を扱えるハンドラー
type AnimalHandler = (animal: Animal) => void;
// 1. 動物全般を処理する関数を用意する
function processAnimals(handler: AnimalHandler) {
// この関数の中では、どんな動物(猫かもしれない!)が来るか分からないので、
// 一般的な Animal を渡して実行する
const genericCat: Animal = { name: “タマ” };
handler(genericCat);
}
// 2. 犬専用のハンドラーを作る
const handleOnlyDog: DogHandler = (dog: Dog) => {
dog.bark(); // 犬専用のメソッドを呼び出している!
};
// 3. さあ、これを渡してみよう……あれ? エラーにならないぞ?
processAnimals(handleOnlyDog); // ⚠️ ここにコンパイルエラーが出てほしい!
おっと……! TypeScriptのデフォルトの設定(あるいは緩い設定)だと、ここがスルーされてしまうことがあります(※厳格な `strictFunctionTypes: true` が有効な場合、最新のTSではここでエラーになりますが、なぜエラーになるべきなのか本質を理解することが重要です)。
何が起きているのか?(実行時エラーの恐怖)
もしこのコードが実行されたら、何が起きるでしょうか?
1. `processAnimals` の中では、猫(`genericCat`:`bark`を持たない)が作られます。
2. `handler(genericCat)` が実行されます。中身は `handleOnlyDog` です。
3. `handleOnlyDog` は、「受け取った引数は必ず `Dog` だ」と信じ込み、`dog.bark()` を呼び出します。
4. 結果:`TypeError: dog.bark is not a function`(クラッシュ!)
猫が来ているのに「ワンと鳴け」と命令してしまったわけですから、当然プログラムは崩壊しますよね。
これが、関数の引数における「反変性(Contravariance)」の罠です。
—
3. なぜ「逆向き」に代入できてしまうのか?(反変性の正体)
数学や集合論の言葉を使わずにかみ砕いて説明すると、こうなります。
- 引数の型は「受け手(消費者)」ではなく「送り手(生産者)」の都合で決まる。
- 関数に渡す引数の型は、「より広い(何でも受け入れられる)型」のほうが安全。
整理してみましょう。
- `AnimalHandler`(引数が `Animal`)は、「猫が来ようが犬が来ようが、動物全般を処理できる懐の深い関数」です。
- `DogHandler`(引数が `Dog`)は、「犬しか処理できないワガママな関数」です。
「犬しか処理できない人」を、「何が来るか分からない動物園の園長(AnimalHandlerを求める場所)」に配置すると、猫が来たときに処理できなくて困ってしまいますよね。
したがって、
- 求める引数:`Animal`(広い)
- 渡す引数:`Dog`(狭い)
この方向の代入(広い場所へ、狭いものを入れる)は、型安全を破壊する危険な行為なのです。
逆に、
- 求める引数:`Dog`(狭い・犬専用の機能があればいい)
- 渡す引数:`Animal`(広い・どんな動物でも扱える)
この方向であれば、猫が来ようが犬が来ようが、動物全般を扱える人が処理するので安全です。
つまり、関数の引数の型においては、代入の方向が「逆転(反変)」するのです。
—
4. 現場で絶対に防ぐための鉄則と回避策
「じゃあ、うっかり危ない関数を渡さないようにするにはどうすればいいの?」という話ですね。
安心してください、現代のTypeScriptには強力な守り神がいます。
鉄則1: `tsconfig.json` で `strictFunctionTypes` を必ず有効にする
TypeScriptのコンパイラオプションである `tsconfig.json` にて、以下の設定が有効になっていることを確認してください。
{
“compilerOptions”: {
“strict”: true
// または個別に
// “strictFunctionTypes”: true
}
}
`strict: true` を入れておけば、先ほどのような「危険な関数の代入」は、TypeScriptがコンパイルエラーとしてビシッと弾いてくれるようになります。
※これがない古いプロジェクトや設定だと、関数型の引数が「共変」として緩く扱われてしまい、冒頭のような実行時エラーの温床になります。
回避策2: ジェネリクスを使って安全性を担保する
もし、柔軟に色々なハンドラーを受け入れたい場合は、型を固定するのではなく、ジェネリクス(総称型)を使って「何を受け取る関数なのか」を正しく伝搬させましょう。
// T という「特定の動物の型」を柔軟に受け入れる設計にする
function processSpecificAnimals
handler(target);
}
const myDog: Dog = { name: “ポチ”, bark: () => console.log(“ワン!”) };
// 犬を渡すなら、犬用ハンドラーでも完全に安全!
processSpecificAnimals((dog: Dog) => {
dog.bark();
}, myDog);
これなら、渡すデータの型(`T`)と、ハンドラーが要求する引数の型が完全に一致するため、TypeScriptの型システムがしっかりと整合性を守ってくれます。
—
まとめ:ここをクリアすればTypeScriptマスターへ一歩前進!
いかがだったでしょうか?
- オブジェクトやプリミティブの代入は 「共変(Covariant)」(広い場所へ狭いものを入れるのはOK)。
- しかし、「関数の引数」の型だけは、安全性のために関係が逆転する 「反変(Contravariant)」 になる。
この「引数の反変性」という概念は、最初は少し頭がこんがらがりやすいポイントです。ですが、「この関数は一体どんな引数を要求しているんだっけ?」「本当にその引数をすべて正しく処理できる安心な関数かな?」と一歩引いて考えるクセをつけるだけで、バグの大部分をコンパイル時に消し去ることができます。
ここを理解できたあなたは、もう単なる「TypeScriptの文法を知っている人」ではありません。「型システムの挙動を深く理解し、堅牢なアーキテクチャを設計できるエンジニア」です!
明日からのコードレビューや設計で、ぜひこの視点を活かしてみてくださいね。それでは、快適なTypeScriptライフを!