TypeScript 中的协变与逆变

345 阅读3分钟

理解 TypeScript 中的协变(covariance)、逆变(contravariance)、子类型(subtype)和父类型(supertype)之间的关系可以帮助我们正确处理类型系统中的泛型和赋值操作。

子类型与父类型

在 TypeScript 中,子类型和父类型的关系是指某个类型(子类型)是否能够安全地替代另一个类型(父类型)使用。具体来说:

  • 子类型(subtype):如果类型 A 能够作为类型 B 的替代,那么称类型 A 是类型 B 的子类型。在 TypeScript 中,这通常意味着可以将 A 的实例赋值给 B 类型的变量或参数。

  • 父类型(supertype):类型 B 被类型 A 替代时,称 B 是 A 的父类型。这意味着 A 拥有 B 的所有特性,并且可能还有额外的特性或行为。

例如,假设有以下类型定义:

interface Animal {
    name: string;
}

interface Dog extends Animal {
    breed: string;
}

这里,DogAnimal 的子类型,因为 Dog 扩展了 Animal 接口,拥有了额外的 breed 属性。

赋值与类型兼容性

在 TypeScript 中,赋值操作的有效性取决于两个类型之间的子类型关系:

  • 类型兼容性:如果类型 A 是类型 B 的子类型(A <: B),那么 A 的实例可以赋值给 B 类型的变量或参数,这称为类型兼容性。

例如:

let animal: Animal;
let dog: Dog = { name: 'Buddy', breed: 'Labrador' };

animal = dog; // 可以赋值,因为 Dog 是 Animal 的子类型

在这个例子中,dogDog 类型的实例,可以安全地赋值给 animal 变量,因为 DogAnimal 的子类型。

协变与逆变

  • 协变(covariance):协变发生在返回类型的情况下。如果类型 A 是类型 B 的子类型(A <: B),那么 Foo<A>Foo<B> 的子类型。

例如:

interface MyFunc<T> {
    (): T;
}

let getAnimal: MyFunc<Animal>;
let getDog: MyFunc<Dog>;

getAnimal = () => ({ name: 'Max' });
getDog = () => ({ name: 'Buddy', breed: 'Labrador' });

getAnimal = getDog; // 协变:因为 getDog 的返回类型 Dog <: Animal

在这个例子中,getDogMyFunc<Dog> 类型的函数,它的返回类型是 Dog。由于 DogAnimal 的子类型,所以 getDog 可以安全地赋值给 getAnimal,展示了协变的特性。

  • 逆变(contravariance):逆变发生在参数类型的情况下。如果类型 A 是类型 B 的子类型(A <: B),那么 (x: B) => void(x: A) => void 的子类型。

例如:

interface MyHandler<T> {
    (x: T): void;
}

let animalHandler: MyHandler<Animal>;
let dogHandler: MyHandler<Dog>;

dogHandler = (dog: Dog) => console.log(dog.breed);
dogHandler = animalHandler; // 逆变:因为 MyHandler<Animal> <: MyHandler<Dog>

在这个例子中,dogHandler 是一个处理 Dog 类型参数的处理函数,可以赋值给 animalHandler,因为 MyHandler<Animal>MyHandler<Dog> 的子类型。逆变使得我们可以用更一般的处理函数来替代更特定的处理函数。

总结

  • 子类型与父类型:描述类型之间的替代关系。
  • 赋值与类型兼容性:在 TypeScript 中,赋值操作的有效性取决于子类型关系。
  • 协变与逆变:分别发生在返回类型和参数类型上,影响了泛型类型的使用灵活性。

理解这些概念可以帮助我们更好地设计和使用泛型接口、函数和类,确保类型安全和代码的灵活性。