TypeScript 中的逆变、协变和双向协变
前言
为什么需要引入逆变、协变和双向协变这些概念
因为考虑到类型兼容
,详情参考https://www.typescriptlang.org/docs/handbook/type-compatibility.html
在 TypeScript 中,有两种兼容性机制:子类型和赋值
(意思是理解成在子类型和赋值这种操作下才会触发兼容性,比如比较该类型是不是其子类型)
出于实际目的,类型兼容性由赋值兼容性决定
,即使在implements and extends子句的情况下也是如此
基础
TypeScript中的类型兼容性可以用于确定一个类型是否可以赋值给其他类型。这里要了解两个概念:
官方文档说到TS 是结构性的类型系统(Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing. Consider the following code)
结构类型:一种只使用其成员来描述类型的方式(类型 ducking type); 名义类型:明确的指出或声明其类型,如c#,java。
TypeScript的类型兼容性就是基于结构子类型的。下面的例子:
interface IName {
name: string;
}
class Man {
name: string;
constructor() {
this.name = "鸣人";
}
}
let p: IName;
p = new Man();
p.name;
上面的代码在TypeScript不会出错,但是在java等语言中就会报错,因为Man类没有明确的说明实现了IName 接口
结构化
在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。而结构性类型系统是基于类型的组成结构,且不要求明确地声明。
TS 是结构性的类型系统
。所谓结构化就是对值所具有的结构进行类型检查。
简单来说,要判断两个类型是否是兼容的,只需要看两个类型的结构是否兼容就可以了,不需要关心类型的名称是否相同
。比如:
interface Pet {
name: string;
}
class Dog {
name: string;
}
let pet: Pet;
// OK, because of structural typing
pet = new Dog();
子类型
比如考虑如下接口:
interface Animal {
age: number
}
interface Dog extends Animal {
bark(): void
}
在这个例子中,Animal 是 Dog 的父类,Dog是Animal的子类型,子类型的属性比父类型更多,更具体。
在类型系统中,属性更多的类型是子类型。
在集合论中,属性更少的集合是子集。
也就是说,子类型是父类型的超集,而父类型是子类型的子集,这是直觉上容易搞混的一点。
记住一个特征,子类型比父类型更加具体,这点很关键。
可赋值性 assignable
assignable 是类型系统中很重要的一个概念,当你把一个变量赋值给另一个变量时,就要检查这两个变量的类型之间是否可以相互赋值。
let animal: Animal
let dog: Dog
animal = dog // ✅ok
dog = animal // ❌error! animal 实例上缺少属性 'bark'
协变和逆变
如何处理类型兼容呢?通过协变和逆变原则
协变与逆变(covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
维基百科上关于协变和逆变的解释有点晦涩难懂。这里,我们用更通俗一点的语言来表述:
协变:允许子类型转换为父类型(可以里式替换LSP原则进行理解) 逆变:允许父类型转换为子类型
逆变
// Dog ≼ Animal
var feedAnimal = (o: Animal) => {};
var feedDog = (o: Dog) => {
o.bark();
};
feedDog = feedAnimal; // 成立,feedAnimal ≼ feedDog
feedAnimal = feedDog; // 严格模式下报错,因为可能animal并不能保证存在bark()
// 也就是存在如下场景
function func(f: typeof feedDog) {
var d: Dog;
f(d);
}
func(feedAnimal);
在函数的参数类型中,是符合逆变的
,函数的关系和参数的关系是相反的
。但在TS中,参数类型是双向协变的(详见下文3.1小节),如果项目里开启了"strict": true,意味着,会来带开启 strictFunctionType ,此时,才按照逆变处理
双向协变
在老版本的 TS 中,函数参数是双向协变的。也就是说,既可以协变又可以逆变,但是这并不是类型安全的。在新版本 TS (2.6+) 中 ,你可以通过开启 strictFunctionTypes 或 strict 来修复这个问题。设置之后,函数参数就不再是双向协变的了。
参考资料
https://juejin.cn/post/7019565189624250404 https://juejin.cn/post/6950254535298252836 why-are-functions-with-fewer-parameters-assignable-to-functions-that-take-more-parameters what are covariance and contravariance