【入門編】関数型における「Covariance(共変性)」と「Contravariance(反変性)」の理解 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの型システムを深く探求する旅へようこそ。
フロントエンドからバックエンドまで、TypeScriptであらゆるアーキテクチャを構築する中で、避けて通れず、そして最も「おや?」とつまずきやすいのが型システムのルール、特に「共変性(Covariance)」と「反変性(Contravariance)」の概念です。

他の言語(JavaやC#など)からやってきた開発者や、TypeScriptを基礎からしっかりモノにしたい方にとって、この「関数の引数を渡すときの型の逆転現象」は最初の大きな山場になります。

でも、安心してください。本質的なルールと、なぜコンパイラがそう判断するのかの背景さえ掴んでしまえば、怖くありません。ここをクリアすれば、あなたのTypeScriptの基本スキルは間違いなく一段階上のステージに到達しますよ。

さあ、一緒にその仕組みを解き明かしていきましょう!

—

1. そもそも「共変」と「反変」ってなに?(直感的なイメージ)

難解な数学用語のように聞こえますが、意味自体はとてもシンプルです。まずは言葉の定義からスッキリ整理しておきましょう。

  • 共変(Covariance / コバリアンス):

「型の階層構造(親子関係)が、そのまま保たれる性質」のことです。
例えば、「犬」は「動物」の子孫(サブタイプ)ですよね。このとき、「犬のリスト」を「動物のリスト」として扱えるような関係性を指します。

  • 反変(Contravariance / コントラバリアンス):

なんと、これが「型の階層構造が逆転する性質」のことです。
「動物を引数にとる関数」と「犬を引数にとる関数」を比べたとき、代入できる方向が逆になります。

「えっ、なぜ逆転するの?」と思いますよね。ここが今回の最大のテーマです。具体的なコードを見る前に、まずは身近な例で脳内イメージを作ってみましょう。

—

2. 実例で見る:なぜ関数の「引数」は型が逆転するのか?

TypeScriptでは、関数を別の関数へ「コールバック」や「引数」として渡す場面が頻繁にあります。ここで発生する「型の安全性の罠」をコードで追ってみましょう。

ここに、「動物(Animal)」という大雑把な型と、「犬(Dog)」という具体的な型があるとします。`Dog`は`Animal`の特殊な姿(サブタイプ)なので、`Animal`が持つプロパティをすべて持っています。

// 1. 階層構造の定義
type Animal = {
name: string;
};

type Dog = {
name: string;
bark: () => void; // 犬には鳴く機能がある
};

思考実験:もし「犬を扱う関数」に「すべての動物を扱う関数」を渡せたら?

ここで、「犬(Dog)を受け取って可愛がる関数」を考えてみます。

// 引数に「犬」を要求する関数
function playWithDog(dogFunc: (dog: Dog) => void) {
const myDog: Dog = { name: “ポチ”, bark: () => console.log(“ワン!”) };

// この関数は、渡された関数に「犬」を渡して実行するつもり
dogFunc(myDog);
}

さて、ここに「どんな動物(Animal)でも受け取って処理できる親切な関数」を用意します。

// すべての動物を扱う関数(Animalを受け取る)
const handleAnyAnimal = (animal: Animal) => {
console.log(`名前は ${animal.name} です。`);
};

「犬専用の遊び場(`playWithDog`)」に、「どんな動物でもOKなスタッフ(`handleAnyAnimal`)」を派遣したらどうなるでしょうか?

// ─── ここがポイント! ───
// 引数に「Animalを受け取る関数」を渡しているのに、
// 「Dogを受け取る関数」を期待する場所にスッと入ります。
playWithDog(handleAnyAnimal); // 🛑 エラーにならない!安全に動く!

`handleAnyAnimal` は `Animal` を受け取ります。今、渡されているのは `Dog` ですが、`Dog` は `Animal` の特徴をすべて満たしているので、名前(`name`)を読み取る処理は100%安全に実行できます。

つまり、「広範囲なデータを受け取れる関数(Animal => void)」は、「狭い範囲(限定されたデータ)を期待する場所(Dog => void)」に代入して使うことができるのです。

これがまさに、「引数の型は、逆向き(反変)にしか代入できない」という現象の正体です。

—

3. 逆にしてみよう:何が危険なのか?

では、この関係を逆にしてみたらどうなるでしょうか?
「犬専用の関数(Dog => void)」を、「どんな動物でも来ると思って待ち構えている場所」に渡してみます。

// どんな動物(Animal)でも受け取ることを期待する場所
function feedAnimal(animalFunc: (animal: Animal) => void) {
const genericAnimal: Animal = { name: “名もなき生き物” };

// この関数は「普通の動物(猫かもしれない、鳥かもしれない)」を渡すつもり
animalFunc(genericAnimal);
}

// 犬専用の処理をする関数(Dogしか受け付けない)
const handleOnlyDog = (dog: Dog) => {
dog.bark(); // 💥 犬固有のメソッドを呼び出している!
};

これを実行すると、どうなるでしょうか?

// 🛑 TypeScriptのコンパイラがここで「待て!」とエラーを出します
feedAnimal(handleOnlyDog);

【何が起きるか(実行時クラッシュの危機)】
1. `feedAnimal` は、「普通の動物(猫かもしれない)」を引数として渡そうとします。
2. しかし、渡された関数 `handleOnlyDog` は、「絶対に犬が来る」と信じ込んでいるため、内部で `dog.bark()` を呼び出します。
3. もしそこに「猫(barkメソッドを持たない)」が渡されてきたら……? 実行時に `TypeError: dog.bark is not a function` が発生してアプリがクラッシュします!

TypeScriptのコンパイラは、このような「実行時エラーの芽」をコンパイル時に完全に摘み取るために、関数の引数において反変性(Contravariance)を厳格に強制しているのです。

—

4. まとめ:頭を整理する図解と覚え方

ここまでの関係性を、スッキリとした図で整理してみましょう。

【型の親子関係】
Animal (親・広い)
▲
│ 拡張・具体的
Dog (子・狭い)

【関数の代入・引数の関係(反変性)】
期待される場所(引数): (dog: Dog) => void
代入できる関数(引数): (animal: Animal) => void ✨OK!広い心で受け入れられるから安全
代入NGな関数(引数): (poodle: Poodle) => void ❌NG!狭すぎて対応できないケースがある

💡 現場で迷わないための黄金律

  • 関数の「引数」の型は、逆向き(反変:Contravariant)に動く
  • 子を期待する場所に、親を引数にとる関数を渡すのは 安全(OK)
  • 親を期待する場所に、子を引数にとる関数を渡すのは 危険(NG)

(※なお、関数の「戻り値(Return Type)」については、そのままの方向で代入できる「共変(Covariant)」になります。戻り値の話は直感的で、「犬を返す関数」は「動物を返す場所」にそのまま使えますよね!)

—

5. 実務での注意点:`strictFunctionTypes` の恩恵

実は、古いバージョンのTypeScriptや、他の言語(TypeScriptの初期仕様など)では、関数の引数は「共変」として緩く扱われていました。しかし、それでは上記のような実行時エラーを防げません。

現在のTypeScriptでは、tsconfig.json の `strict` モード(または個別の `strictFunctionTypes: true`)によって、この関数の引数が厳密に反変としてチェックされるようになっています。

{
“compilerOptions”: {
“strict”: true // これが有効であれば、今回の反変性チェックがフルに働きます
}
}

もし実務で、コールバック関数の型定義を行っていて「なぜか親の型を要求する場所に、子の型を持つ関数を渡すと型エラーになるぞ……?」と悩んだときは、この「引数の反変性」のルールを思い出してください。

—

最後に:TypeScriptの厳格さは、あなたの最強の味方

今回は、少し頭がウッとなるような「共変性と反変性」について解説しました。
一見すると難解なルールに思えますが、これはすべて「実行時エラーというバグを、TypeScriptのコンパイラに事前に発見してもらうため」の極めて合理的な仕組みです。

ここをクリアできれば、複雑なジェネリクスや高度な型ユーティリティを設計する際も、迷うことなく意図した通りの堅牢なコードが書けるようになります。

日々のコーディングで「おっ、これは反変のルールが適用されているな」とニヤリとできるようになったら、あなたも立派なTypeScriptマエストロです。
一緒に、安全で美しい型安全な世界を極めていきましょう!

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