高级:TypeScript 中的类型关系控制
1. 什么是协变与逆变
协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
许多程序设计语言的类型系统支持子类型。例如,如果 Dog
是 Animal
的子类型,那么 Dog
类型的表达式可用于任何出现 Animal
类型表达式的地方。
所谓的变型(variance)是指如何根据组成类型之间的子类型关系,来确定更复杂的类型之间(例如 List<Dog>
之于 List<Animal>
,回传 Dog
的函数之于回传 Animal
的函数等等)的子类型关系。当我们用类型构造出更复杂的类型,原本类型的子类型性质可能被保持、反转、或忽略───取决于类型构造器的变型性质。
在一门程序设计语言的类型系统中,一个类型规则或者类型构造器是:
- 协变(covariant),如果它保持了子类型序关系≦。该序关系是:子类型≦基类型。
- 逆变(contravariant),如果它逆转了子类型序关系。
- 不变(invariant),如果上述两种均不适用。
2. 协变 (Covariance)
协变是最符合直觉的子类型关系,当 Dog extends Animal
时:
ts
type Animals = Animal[]
type Dogs = Dog[]
const dogs: Dogs = [new Dog()]
const animals: Animals = dogs // 允许协变赋值
数组类型表现出协变特性,但存在安全隐患:
ts
class Cat extends Animal {}
function addCat(animals: Animal[]) {
animals.push(new Cat()) // 编译通过
}
const dogs: Dogs = []
addCat(dogs) // 未报错,但未预期的 Dog[] 被插入 Cat 对象
dogs[0].bark() // 运行时错误:Cat 实例没有 bark 方法
这种设计权衡了类型安全与开发便利性,通过 readonly
修饰符可强制不可变:
ts
const readonlyDogs: readonly Dog[] = [new Dog()]
const readonlyAnimals: readonly Animal[] = readonlyDogs // 安全协变
3. 逆变 (Contravariance)
函数参数表现出逆向类型关系,考虑事件处理器类型:
ts
type AnimalHandler = (animal: Animal) => void
type DogHandler = (dog: Dog) => void
// 默认双变模式下的赋值
declare let animalHandler: AnimalHandler
declare let dogHandler: DogHandler
animalHandler = dogHandler // 允许(不安全协变)
dogHandler = animalHandler // 允许(逆变)
启用严格函数类型检查后:
ts
// tsconfig.json 中设置 "strictFunctionTypes": true
animalHandler = dogHandler // 错误dogHandler = animalHandler // 允许(正确逆变)
这种模式通过类型参数约束实现安全控制:
ts
interface Comparator<T> {
compare(a: T, b: T): number
}
const animalComparator: Comparator<Animal> = { /*...*/ }
const dogComparator: Comparator<Dog> = animalComparator // 需要逆变支持
4. 类型参数控制
通过 in
和 out
关键字实现显式变型控制(TypeScript 4.7+):
ts
interface Producer<out T> {
produce(): T
}
interface Consumer<in T> {
consume(item: T): void
}
// 正确使用变型
const dogProducer: Producer<Dog> = { produce: () => new Dog() }
const animalProducer: Producer<Animal> = dogProducer // 协变安全
const animalConsumer: Consumer<Animal> = { consume: (a) => { a.move() } }
const dogConsumer: Consumer<Dog> = animalConsumer // 逆变安全
这种显式声明方式通过编译器强制验证类型关系的安全性。
5. 复杂类型组合
联合类型与交叉类型的变型行为:
ts
// 联合类型表现为协变
type A = Dog | Cat
type B = Animal | Cat
const a: A = new Dog()
const b: B = a // 允许
// 交叉类型参数逆变
type FnA = (x: Animal) => void
type FnB = (x: Dog) => void
const fn: FnA & FnB = (x) => { x.move() } // 参数类型为 Animal & Dog
6. 设计模式应用
观察者模式中的安全实现:
ts
class Observable<T> {
private observers: ((value: T) => void)[] = []
// 使用逆变参数类型
subscribe(observer: (value: T) => void): void {
this.observers.push(observer)
}
notify(value: T) {
this.observers.forEach(obs => obs(value))
}
}
const numberObservable = new Observable<number>()
const numericObserver = (n: number | string) => console.log(n)
numberObservable.subscribe(numericObserver) // 需要关闭 strictFunctionTypes
通过泛型约束实现安全事件处理:
ts
interface Event<T extends EventTarget> {
readonly target: T
}
interface MouseEvent extends Event<HTMLElement> {
// 新增鼠标事件属性
}
function handleEvent<T extends EventTarget>(event: Event<T>) {
// 类型安全的事件处理
}
深入理解这些类型关系机制,可以帮助开发者:
- 设计更安全的泛型接口
- 合理配置编译器选项
- 优化复杂类型定义
- 预防潜在的类型安全漏洞
- 提升类型推导的准确性
实际开发中建议结合 strict
模式,通过类型测试验证关键类型的变型行为,在灵活性和安全性之间取得平衡。