こんにちは。TypeScriptの深淵へようこそ。
現場でコードを書いていると、「なぜこの型は代入できるのに、あちらはエラーになるんだ?」と頭を抱える瞬間がありますよね。その正体こそ、今回解説する「変位(Variance)」という概念です。
多くのエンジニアが感覚でやり過ごすこの壁を突破できれば、TypeScriptの型システムを「魔法」ではなく「論理」として操れるようになります。さあ、一緒に深掘りしていきましょう。
—
1. 「共変」と「反変」:直感の裏側にある論理
まず、言葉の定義から。専門用語に怯える必要はありません。
- 共変 (Covariance): 型の継承関係(親子関係)がそのまま維持されること。
- 反変 (Contravariance): 型の継承関係が「逆転」すること。
TypeScriptにおいて、オブジェクトや戻り値は「共変」ですが、関数の引数は「反変」です。ここが、多くの開発者が混乱する最大のポイントです。
なぜ引数は「反変」なのか?
「親クラスの型を受け取れる関数は、子クラスも受け取れるはず(共変)」と考えがちですが、TypeScriptはあえて逆の挙動を要求します。
イメージしてみてください。あなたは「哺乳類(Animal)なら何でも食べる」という関数を用意しました。ここに「犬(Dog)だけを食べる」という関数を代入しようとするとどうなるでしょう?
type Animal = { name: string };
type Dog = Animal & { bark: () => void };
// 「何でも食べる」関数
let feedAnimal: (a: Animal) => void = (a) => console.log(`Eating ${a.name}`);
// 「犬だけ食べる」関数
let feedDog: (d: Dog) => void = (d) => {
d.bark(); // ここで犬特有のメソッドを呼んでいる!
};
// もしこれが許されると…
feedAnimal = feedDog;
// 実行時:Dog型しか扱えない関数に、猫(Cat)を渡したらどうなる?
// feedAnimal({ name: “Cat” }); // Catには bark() がない! -> 実行時クラッシュ!
お分かりですね。「引数の型をより広げる(抽象度を上げる)」ことは安全ですが、「引数の型をより狭める(具体化する)」ことは、実行時の型安全性を破壊するのです。だからこそ、TypeScriptでは代入の際に「引数の型は、元の型よりも広くなければならない(反変)」という厳しい制約を課しているのです。
—
2. 実務で遭遇するエラーの正体
現場でよく見る「Type … is not assignable to type …」というエラー。引数の型定義を少し変えるだけで発生するのは、この反変性が働いているからです。
陥りやすい罠:コールバック関数の型定義
interface Processor {
run: (data: Animal) => void;
}
// 失敗例:引数をDogに限定しようとする
const myProcessor: Processor = {
run: (data: Dog) => { // エラー!
// Property ‘bark’ is missing in type ‘Animal’
}
};
「私はDog型しか扱わないんだから、これでいいじゃないか!」と思うかもしれません。しかし、`Processor`のインターフェースは「どんなAnimalでも処理できること」を保証しているため、それより狭い`Dog`しか受け取れない関数を割り当てることは、契約違反(リスコフの置換原則違反)となるわけです。
—
3. この壁を乗り越えるための極意
この制約を理解すると、型定義の書き方がガラリと変わります。
1. 引数は「広く」、戻り値は「狭く」
関数を作る際は、引数はできるだけ抽象的に(広く)、戻り値はできるだけ具体的に(狭く)定義するのがベストプラクティスです。
2. どうしても狭い型を扱いたい場合
もしコールバック関数で特定の型(Dog)を扱いたい場合は、型ガードやアサーションを使いつつ、引数定義自体は親クラス(Animal)に合わせておくのが安全です。
const safeProcessor: Processor = {
run: (data: Animal) => {
// 実行時に型を確認してからキャストする
if (‘bark’ in data) {
(data as Dog).bark();
}
}
};
—
最後に:TypeScriptは「守ってくれている」
この「反変性」という仕組みは、一見すると開発を制限する煩わしいルールに見えるかもしれません。しかし、これはTypeScriptが「コンパイル後の実行時クラッシュを未然に防ぐ」ために、あなたの書いたコードを全力で守ろうとしている証拠です。
この理論を理解すれば、もう型エラーで立ち止まることはありません。エラーが出たとき、「ああ、これは引数の型が狭すぎる(反変性のルールに反している)んだな」と瞬時に見抜けるようになれば、あなたはもうTypeScriptのマスターへの道を歩んでいます。
ここをクリアしたあなたは、もう初心者ではありません。自信を持って、さらに深いTypeScriptの世界へ突き進んでくださいね!