TypeScript 协变与逆变
协变与逆变是 TypeScript 类型系统中的重要概念,理解它们有助于编写类型安全的代码。
它们描述了泛型类型在父子类型关系中的行为。
为什么需要协变与逆变
当我们使用泛型类或函数时,类型参数的行为并不像你想象的那么简单。
将 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 时考虑协变与逆变,编写更安全的类型代码。
