深入学习 TypeScript 类型系统的话,逆变、协变、双向协变、不变是绕不过去的概念。
类型安全和型变
TypeScript 给 JavaScript 添加了一套静态类型系统,是为了保证类型安全的,也就是保证变量只能赋同类型的值,对象只能访问它有的属性、方法。
比如 number 类型的值不能赋值给 boolean 类型的变量,Date 类型的对象就不能调用 exec 方法。
这是类型检查做的事情,遇到类型安全问题会在编译时报错。
但是这种类型安全的限制也不能太死板,有的时候需要一些变通,比如子类型是可以赋值给父类型的变量的,可以完全当成父类型来使用,也就是“型变(variant)”(类型改变)。
这种“型变”分为两种,一种是子类型可以赋值给父类型,叫做协变(covariant),一种是父类型可以赋值给子类型,叫做逆变(contravariant)。
先来看下协变:
协变(covariant)
协变是指子类型可以替代父类型的情况。
其中协变是很好理解的,比如我们有两个 interface:
interface Person {
name: string;
age: number;
}
interface Guang {
name: string;
age: number;
hobbies: string[]
}
这里 Guang 是 Person 的子类型,更具体,那么 Guang 类型的变量就可以赋值给 Person 类型:
let person: Person = {
name: '张三',
age: 30
};
let guang: Guang = {
name: '李四',
age: 25,
hobbies: ['读书', '运动']
};
// 由于 Guang 是 Person 的子类型,所以可以将 Guang 类型的变量赋值给 Person 类型的变量
person = guang;
// 但是不能将 Person 类型的变量赋值给 Guang 类型的变量,因为 Person 缺少 hobbies 属性
// guang = person; // 编译错误:Property 'hobbies' is missing in type 'Person' but required in type 'Guang'.
这并不会报错,虽然这俩类型不一样,但是依然是类型安全的。
这种子类型可以赋值给父类型的情况就叫做协变。
逆变(contravariant)
逆变是指在函数参数类型中,父类型的参数可以替代子类型的参数。
我们有这样两个函数:
let printHobbies: (guang: Guang) => void;
printHobbies = (guang) => {
console.log(guang.hobbies);
}
let printName: (person: Person) => void;
printName = (person) => {
console.log(person.name);
}
printHobbies 的参数 Guang 是 printName 参数 Person 的子类型。
那么问题来了,printName 能赋值给 printHobbies 么?printHobbies 能赋值给 printName 么?
测试一下发现是这样的:
interface Person {
name: string;
age: number;
}
interface Guang {
name: string;
age: number;
hobbies: string[];
}
let printHobbies: (guang: Guang) => void;
printHobbies = (guang) => {
console.log(guang.hobbies);
};
let printName: (person: Person) => void;
printName = (person) => {
console.log(person.name);
};
// 由于 Person 是 Guang 的父类型,可以将 printName 函数赋值给 printHobbies 函数
printHobbies = printName;
// 但是不能将 printHobbies 函数赋值给 printName 函数,因为 Person 类型的参数缺少 hobbies 属性
// printName = printHobbies; // 编译错误:Type '(guang: Guang) => void' is not assignable to type '(person: Person) => void'.
解释
-
类型兼容性:
Guang
是Person
的子类型,因为Guang
包含了Person
的所有属性,并且添加了一个额外的属性hobbies
。- 在函数参数类型中,
Person
类型的参数可以替代Guang
类型的参数,因为Person
类型的参数可以接受任何Guang
类型的参数。
-
赋值规则:
printHobbies = printName
是允许的,因为printName
函数可以接受Person
类型的参数,而Guang
是Person
的子类型。printName = printHobbies
是不允许的,因为printHobbies
函数期望Guang
类型的参数,而Person
类型的参数可能缺少hobbies
属性。
总结
ts 通过给 js 添加了静态类型系统来保证了类型安全,大多数情况下不同类型之间是不能赋值的,但是为了增加类型系统灵活性,设计了父子类型的概念。父子类型之间自然应该能赋值,也就是会发生型变(variant)。
型变分为逆变(contravariant)和协变(covariant)。协变很容易理解,就是子类型赋值给父类型。逆变主要是函数赋值的时候函数参数的性质,参数的父类型可以赋值给子类型,这是因为按照子类型来声明的参数,访问父类型的属性和方法自然没问题,依然是类型安全的。但反过来就不一定了。
不过 ts 2.x 之前反过来依然是可以赋值的,也就是既逆变又协变,叫做双向协变。
为了更严格的保证类型安全,ts 添加了 strictFunctionTypes 的编译选项,开启以后函数参数就只支持逆变,否则支持双向协变。
型变都是针对父子类型来说的,非父子类型自然就不会型变也就是不变(invariant)。
ts 中父子类型的判定是按照结构来看的,更具体的那个是子类型。