TypeScript 共変と反変
共変と反変は TypeScript の型システムにおける重要な概念であり、これらを理解することで型安全なコードを書くことができます。
これらは、ジェネリック型が親子型関係の中でどのように振る舞うかを説明します。
SVG 図解:共変と反変
なぜ共変と反変が必要なのか
ジェネリッククラスや関数を使用するとき、型パラメータの動作は想像ほど単純ではありません。
Dog を Animal に代入するのは安全ですが、Animal を処理する関数を Dog を処理する関数に代入するのは安全ではない可能性があります。
共変と反変のルールは、TypeScript がこれらの潜在的な型エラーを検出するのに役立ちます。
概念:共変はサブタイプからスーパータイプへの変換を許可し、反変はスーパータイプからサブタイプへの変換を許可し、不変はいずれの方向の変換も許可しません。
共変 (Covariant)
共変とは、サブタイプをスーパータイプに代入できることです。出力型(関数の戻り値など)にとって、これは安全です。
例
class Animal {
name: string = "动物";
}
class Dog extends Animal {
breed: string = "田园犬";
}
// 共変:出力型はより広い型に変換できる
// Dog を返す関数は Animal を返す関数に代入できます
type AnimalGetter = () => Animal;
type DogGetter = () => Dog;
// Dog は Animal のサブクラスなので、DogGetter は AnimalGetter に代入できます。
const getDog: DogGetter = () => new Dog();
const getAnimal: AnimalGetter = getDog; // 协变:安全
// 运行
const animal: Animal = getAnimal();
console.log("动物名称: " + animal.name);
実行結果:
動物名: 動物
戻り値の共変:関数がより具体的なサブタイプを返すのは安全です。なぜなら、返されるオブジェクトは必ずスーパータイプの要件を満たすからです。
反変 (Contravariant)
反変とは、入力型(関数の引数など)において、スーパータイプをサブタイプに代入できることです。
例
class Animal {
name: string = "动物";
}
class Dog extends Animal {
breed: string = "田园犬";
}
// 反変:入力型はより具体的な型に変換できる
// Animal を受け取る関数は Dog を受け取る関数に代入できます
type DogConsumer = (dog: Dog) => void;
type AnimalConsumer = (animal: Animal) => void;
// より広い型を受け入れる関数を、より具体的な型の関数に代入できる場合
// つまり、Dog を渡すと、関数は処理できないかもしれません(Dog 固有の属性が不足しているため)。
const consumeAnimal: AnimalConsumer = (animal) => {
console.log("处理动物: " + animal.name);
};
const consumeDog: DogConsumer = consumeAnimal; // 逆变:安全
// 运行
const dog = new Dog();
dog.breed = "哈士奇";
consumeDog(dog);
引数の反変:関数の引数は反変です。なぜなら、より具体的な型を受け取る関数は、より広い型を処理できないからです。
厳格な関数型を有効にする
TypeScript はデフォルトで関数の引数に対して反変チェックを行います。strictFunctionTypes を有効にすると、このルールが強制されます。
例
interface Animal {
readonly name: string;
}
interface Dog extends Animal {
readonly breed: string;
}
// 定义函数类型
type GetName = (animal: Animal) => string;
type GetDogBreed = (dog: Dog) => string;
// 正しい代入
const getDogBreed: GetDogBreed = (dog) => dog.breed;
// 代入を試みる - strictFunctionTypes ではエラーになる
// AnimalConsumer(パラメータがより広い)は DogConsumer(パラメータがより具体的)に代入できないため
// これはなぜなら参数である逆变の
function printAnimalName(animal: Animal): string {
return animal.name;
}
// より広い型を受け入れる関数を、より具体的な型に代入しようとする
// const getSpecific: GetDogBreed = printAnimalName; // 错误!
console.log("犬种: " + getDogBreed({ name: "旺财", breed: "哈士奇" }));
strictFunctionTypes:tsconfig.json でこのオプションを有効にすると、より厳密な型チェックが可能になります。
ジェネリッククラスの共変
ジェネリッククラスのプロパティはデフォルトで共変です。
例
class Animal {
name: string = "动物";
}
class Dog extends Animal {
breed: string = "狗";
}
// 泛型容器类
class Cage<T> {
animal: T;
constructor(animal: T) {
this.animal = animal;
}
}
// 协变:可以子类型容器赋值给父类型容器
const dogCage = new Cage(new Dog());
const animalCage: Cage<Animal> = dogCage; // 协变:安全
// animalCage は現在、動物を入れるケージとして安全に使用できます。
console.log("动物名称: " + animalCage.animal.name);
プロパティの共変:オブジェクトのプロパティは共変であり、サブタイプのプロパティはスーパータイプのプロパティに代入できます。
配列の共変
TypeScript では配列は共変ですが、可変性によって生じる問題に注意する必要があります。
例
class Animal {
name: string = "动物";
}
class Dog extends Animal {
breed: string = "狗";
}
// 数组协变
const dogs: Dog[] = [
{ name: "旺财", breed: "哈士奇" },
{ name: "小白", breed: "萨摩耶" }
];
// Dog[] 可以赋值给 Animal[]
const animals: Animal[] = dogs; // 协变:安全
// 问题:虽然类型上安全,但实际上可以添加其他动物
// animals.push({ name: "猫咪", breed: "猫" }); // 运行时可能出问题!
console.log("动物数量: " + animals.length);
配列の可変性:共変の代入後に配列を変更すると実行時エラーが発生する可能性があるため、注意が必要です。
extends を使った安全な代入
共変と反変を理解すると、ジェネリックインターフェースを安全に設計できます。
例
interface Producer<T> {
// 生成方法:戻り値は共変です
produce(): T;
}
interface Consumer<T> {
// 消費方法:パラメータは反変です
consume(value: T): void;
}
// 具体实现
class DogProducer implements Producer<Dog> {
produce(): Dog {
return { name: "旺财", breed: "哈士奇" };
}
}
class AnimalConsumer implements Consumer<Animal> {
consume(animal: Animal): void {
console.log("消费动物: " + animal.name);
}
}
// Producer<Dog> 可以赋值给 Producer<Animal>(协变)
const animalProducer: Producer<Animal> = new DogProducer();
// Consumer<Animal> 可以赋值给 Consumer<Dog>(逆变)
const dogConsumer: Consumer<Dog> = new AnimalConsumer();
// 测试
const animal = animalProducer.produce();
console.log("生产: " + animal.name);
dogConsumer.consume({ name: "旺财", breed: "哈士奇" });
設計原則:メソッドの用途に応じて適切な型の方向を選択し、API の型安全性を高めます。
注意事項
- 戻り値の共変:関数がサブタイプを返すのは安全です
- 引数の反変:関数の引数にスーパータイプを使うのは安全です
- 厳格モードの有効化:strictFunctionTypes を使用してより厳密なチェックを行う
- 配列の共変:可変性によって生じる潜在的な問題に注意する
ベストプラクティス:共変と反変を理解することは、より型安全な API を設計し、実行時エラーを回避するのに役立ちます。
まとめ
共変と反変は TypeScript の型システムの中核となる概念です。
- 共変:サブタイプ → スーパータイプ、出力型に使用
- 反変:親型 → 子型、入力型に使用
- 不変:相互に代入できない
- strictFunctionTypes:厳格な関数型チェックを有効にする
その他の拡張提案:ジェネリックAPIを設計する際は共変と反変を考慮し、より安全な型コードを書くこと。