关于前端:面试官说说TypeScript类型兼容协变和逆变

30次阅读

共计 2688 个字符,预计需要花费 7 分钟才能阅读完成。

小册

这是我整顿的学习材料,十分零碎和欠缺,欢送一起学习

  • 古代 JavaScript 高级小册
  • 深入浅出 Dart
  • 古代 TypeScript 高级小册
  • linwu 的算法笔记📒

类型兼容:协变和逆变

引言

在类型零碎中,协变和逆变是对类型比拟 ( 类型兼容 ) 一种形式化形容。在一些类型零碎中,例如 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 类型零碎的重要工具,但咱们必须在灵活性和类型平安之间找到适合的均衡。

正文完
 0