协变与逆变基础概念
TypeScript 中的协变和逆变是类型系统中非常重要的概念, 协变 (Covariance) 和逆变 (Contravariance) 描述了类型层次结构中的子类型关系如何影响复合类型的子类型关系。
- 协变:如果 A 是 B 的子类型,那么
F<A>也是F<B>的子类型,类型关系保持一致方向。 - 逆变:如果 A 是 B 的子类型,那么
F<B>是F<A>的子类型,类型关系方向相反。
TypeScript 中的实际应用
1. 数组和对象的协变
在 TypeScript 中,数组和对象属性是协变的:
// 假设 Dog 是 Animal 的子类型
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// 协变示例
let animals: Animal[] = [];
let dogs: Dog[] = [];
// 这是允许的,因为 Dog[] 是 Animal[] 的子类型
animals = dogs; // OK
// 这是不允许的
dogs = animals; // 错误 - Animal[] 不是 Dog[] 的子类型
2. 函数参数的逆变
函数参数是逆变的。这意味着如果你有一个接受更通用类型的函数,你可以在需要更具体类型函数的地方使用它:
// 函数类型
type AnimalCallback = (animal: Animal) => void;
type DogCallback = (dog: Dog) => void;
let animalFn: AnimalCallback = (animal) => console.log(animal.name);
let dogFn: DogCallback = (dog) => {
console.log(dog.name);
console.log(dog.breed);
};
// 这是允许的 - 可以用处理 Animal 的函数来处理 Dog
dogFn = animalFn; // OK
// 这是不允许的 - 不能用需要 Dog 特有属性的函数来处理所有 Animal
animalFn = dogFn; // 错误
3. 函数返回值的协变
函数返回值是协变的。这意味着函数可以返回比声明的返回类型更具体的类型:
type AnimalFactory = () => Animal;
type DogFactory = () => Dog;
let createAnimal: AnimalFactory = () => ({ name: "动物" });
let createDog: DogFactory = () => ({ name: "狗", breed: "哈士奇" });
// 这是允许的 - 可以用返回 Dog 的函数替代返回 Animal 的函数
createAnimal = createDog; // OK
// 这是不允许的
createDog = createAnimal; // 错误
为什么需要协变和逆变?
这些规则确保了类型安全:
- 协变的数组:当你期望 Animal 数组时,使用 Dog 数组是安全的,因为每个 Dog 都是 Animal。
- 逆变的函数参数:当你需要处理 Dog 的函数时,使用一个能处理任何 Animal 的函数是安全的,因为它不会尝试访问 Dog 特有的属性。
- 协变的返回值:当你期望函数返回 Animal 时,函数返回更具体的 Dog 是安全的。
TypeScript 中的双变(Bivariant)
在早期版本的 TypeScript 中,函数参数既是协变又是逆变的(双变的),这可能导致类型不安全。在现代 TypeScript 中,你可以使用 --strictFunctionTypes 编译器选项来强制函数参数只能是逆变的,从而提高类型安全性。