头图

小册

这是我整理的学习资料,非常系统和完善,欢迎一起学习

类型兼容:协变和逆变

引言

在类型系统中,协变和逆变是对类型比较(类型兼容)一种形式化描述。在一些类型系统中,例如 Java,这些概念是显式嵌入到语言中的,例如使用extends关键字表示协变,使用super关键字表示逆变。在其他一些类型系统中,例如 TypeScript,协变和逆变的规则是隐式嵌入的,通过类型兼容性检查来实现。

协变和逆变的存在使得类型系统具有更大的灵活性。例如,如果你有一个Animal类型的数组,并且你有一个Dog类型的对象(假设DogAnimal的子类型),那么你应该能够将Dog对象添加到Animal数组中。这就是协变。反过来,如果你有一个处理Animal类型对象的函数,并且你有一个Dog类型的对象,你应该可以使用这个函数来处理Dog对象。这就是逆变。

协变和逆变还可以帮助我们创建更通用的代码。例如,如果你有一个可以处理任何Animal的函数,那么这个函数应该能够处理任何Animal的子类型。这意味着,你可以编写一段只依赖于Animal类型的代码,然后使用这段代码处理任何Animal的子类型。

协变(Covariance)

协变描述的是如果存在类型A和B,并且A是B的子类型,那么我们就可以说由A组成的复合类型(例如Array<A>或者(a: A) => void)也是由B组成的相应复合类型(例如Array<B>或者(b: B) => void)的子类型。

让我们通过一个例子来理解协变。假设我们有两个类型AnimalDog,其中DogAnimal的子类型。

type Animal = { name: string };
type Dog = Animal & { breed: string };

let dogs: Dog[] = [{ name: "Fido", breed: "Poodle" }];
let animals: Animal[] = dogs;  // OK because Dog extends Animal, Dog[] is a subtype of Animal[]

这里我们可以将类型为Dog[]dogs赋值给类型为Animal[]animals,因为Dog[]Animal[]的子类型,所以数组是协变的。

协变:类型的向下兼容性

协变是类型系统中的一个基本概念,它描述的是类型的“向下兼容性”。如果一个类型A可以被看作是另一个类型B的子类型(即A可以被安全地用在期望B的任何地方),那么我们就说A到B是协变的。这是类型系统中最常见和直观的一种关系,例如在面向对象编程中的继承就是协变的一种表现。

在TypeScript中,所有的类型都是自身的子类型(即每个类型到自身是协变的),并且nullundefined类型是所有类型的子类型。除此之外,接口和类也可以通过继承来形成协变关系。

class Animal {
  name: string;
}

class Dog extends Animal {
  breed: string;
}

let myDog: Dog = new Dog();
let myAnimal: Animal = myDog;  // OK,因为Dog是Animal的子类型

这个例子中,我们可以将一个Dog对象赋值给一个Animal类型的变量,因为DogAnimal是协变的。

在TypeScript中,泛型类型也是协变的。例如,如果类型A是类型B的子类型,那么Array<A>就是Array<B>的子类型。

let dogs: Array<Dog> = [new Dog()];
let animals: Array<Animal> = dogs;  // OK,因为Array<Dog>是Array<Animal>的子类型

逆变(Contravariance)

逆变是协变的反面。如果存在类型A和B,并且A是B的子类型,那么我们就可以说由B组成的某些复合类型是由A组成的相应复合类型的子类型。

这在函数参数中最常见。让我们来看一个例子:

type Animal = { name: string };
type Dog = Animal & { breed: string };

let dogHandler = (dog: Dog) => { console.log(dog.breed); }
let animalHandler: (animal: Animal) => void = dogHandler;  // Error! 

在这个例子中,我们不能将类型为(dog: Dog) => voiddogHandler赋值给类型为(animal: Animal) => voidanimalHandler。因为如果我们传递一个Animal(并非所有的Animal都是Dog)给animalHandler,那么在执行dogHandler函数的时候,就可能会引用不存在的breed属性。因此,函数的参数类型是逆变的。

逆变:类型的向上兼容性

逆变描述的是类型的“向上兼容性”。如果一个类型A可以被看作是另一个类型B的超类型(即B可以被安全地用在期望A的任何地方),那么我们就说A到B是逆变的。在函数参数类型的兼容性检查中,TypeScript使用了逆变。

type Handler = (arg: Dog) => void;

let animalHandler: Handler = (animal: Animal) => { /* ... */ };
let dogHandler: Handler = (dog: Dog) => { /* ... */ };  // OK,因为Animal是Dog的超类型

这个例子中,我们可以将一个处理`

Dog的函数赋值给一个处理Animal的函数类型的变量,因为AnimalDog的超类型,所以(dog: Dog) => void类型是(animal: Animal) => void`类型的子类型。

这看起来可能有些反直觉,但实际上是为了保证类型安全。因为在执行dogHandler函数时,我们可以安全地传入一个Animal对象,而不需要担心它可能不是Dog类型。

协变与逆变的平衡

协变和逆变在大多数情况下都可以提供合适的类型检查,但是它们并非完美无缺。在实际应用中,我们必须关注可能的边界情况,以避免运行时错误。在某些情况下,我们甚至需要主动破坏类型的协变或逆变,以获得更强的类型安全。例如,如果我们需要向一个Dog[]数组中添加Animal对象,我们可能需要将这个数组的类型声明为Animal[],以防止添加不兼容的类型。

总的来说,协变和逆变是理解和应用TypeScript类型系统的重要工具,但我们必须在灵活性和类型安全之间找到合适的平衡。


linwu
41 声望11 粉丝

☀高级前端开发工程师