带你读《现代TypeScript高级教程》十三、类型兼容:协变和逆变(1)https://developer.aliyun.com/article/1348454?groupCode=tech_library
3.逆变(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) => void的dogHandler赋值给类型为(animal: Animal) => void的animalHandler。因为如果我们传递一个Animal(并非所有的Animal都是Dog)给animalHandler,那么在执行dogHandler函数的时候,就可能会引用不存在的breed属性。因此,函数的参数类型是逆变的。
4.逆变:类型的向上兼容性
逆变描述的是类型的“向上兼容性”。如果一个类型A可以被看作是另一个类型B的超类型(即B可以被安全地用在期望A的任何地方),那么我们就说A到B是逆变的。在函数参数类型的兼容性检查中,TypeScript使用了逆变。
type Handler = (arg: Animal) => void; let animalHandler: Handler = (animal: Animal) => { /* ... */ };let dogHandler: Handler = (dog: Dog) => { /* ... */ }; // OK,因为Animal是Dog的超类型
这个例子中,我们可以将一个处理`Dog的函数赋值给一个处理Animal的函数类型的变量,因为Animal是Dog的超类型,所以(dog: Dog) => void类型是(animal: Animal) => void`类型的子类型。
这看起来可能有些反直觉,但实际上是为了保证类型安全。因为在执行dogHandler函数时,我们可以安全地传入一个Animal对象,而不需要担心它可能不是Dog类型。
5.协变与逆变的平衡
协变和逆变在大多数情况下都可以提供合适的类型检查,但是它们并非完美无缺。在实际应用中,我们必须关注可能的边界情况,以避免运行时错误。在某些情况下,我们甚至需要主动破坏类型的协变或逆变,以获得更强的类型安全。例如,如果我们需要向一个Dog[]数组中添加Animal对象,我们可能需要将这个数组的类型声明为Animal[],以防止添加不兼容的类型。
总的来说,协变和逆变是理解和应用TypeScript类型系统的重要工具,但我们必须在灵活性和类型安全之间找到合适的平衡。