TypeScript 中的协变、逆变、双变和不变是什么?strictFunctionTypes 改变了什么?

先理解原理和示例,再练习完整口述与追问。

设 Dog 是 Animal 的子类型:协变指 T<Dog> 可赋给 T<Animal>,如数组、返回值;逆变方向相反,函数参数位置是逆变的;双变两个方向都允许;不变两个方向都不允许。开启 strictFunctionTypes 后,函数类型属性的参数按逆变检查,但方法简写语法仍是双变。

一个接收 Animal 的处理函数可以安全地当作“接收 Dog 的函数”使用,因为传入的 Dog 一定是 Animal;反过来,接收 Dog 的函数被当成接收 Animal 的函数时,可能收到 Cat 并访问不存在的方法,所以参数是逆变的。返回值则相反:返回 Dog 的函数可以替代返回 Animal 的函数,是协变的。

TypeScript 为了兼容大量已有代码(如 DOM 事件回调),在早期把参数检查设为双变,这是不安全的。strictFunctionTypes(包含在 strict 中)把 fn: (x: T) => void 这种属性写法改为逆变;method(x: T): void 方法简写仍然双变,以保持 Array<T> 等内置类型可用。TypeScript 数组是协变的,但可变数组严格说并不安全。TS 4.7 起可用 in、out 显式标注泛型参数的变型。

ts
interface Animal { name: string }
interface Dog extends Animal { bark(): void }

let handleAnimal = (animal: Animal) => console.log(animal.name)
let handleDog = (dog: Dog) => dog.bark()
handleDog = handleAnimal       // 允许:参数逆变
// handleAnimal = handleDog    // strictFunctionTypes 下报错

const dogs: Dog[] = []
const animals: Animal[] = dogs // 允许:数组协变
animals.push({ name: 'cat' })  // 编译通过,但 dogs 中混入了没有 bark 的对象

最后一行展示了数组协变的不安全之处,这是 TypeScript 有意的取舍。

把第四种叫“抗变”不准确,通常称不变;用 readonly 数组可以减少协变带来的写入风险。

假设 Dog 是 Animal 的子类型:协变是 T<Dog> 能赋给 T<Animal>,比如返回值和数组;逆变方向相反,函数参数就是逆变,因为接收 Animal 的函数可以安全地处理 Dog;双变是两个方向都行,不变是都不行。TS 早期参数检查是双变的,开启 strictFunctionTypes 后函数类型属性的参数按逆变检查,但方法简写仍是双变。数组协变其实不完全安全,可以用 readonly 数组降低风险。