Skip to content

高级:TypeScript 中的类型关系控制

1. 什么是协变与逆变

协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。

许多程序设计语言的类型系统支持子类型。例如,如果 DogAnimal 的子类型,那么 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
// 错误
Type 'DogHandler' is not assignable to type 'AnimalHandler'. Types of parameters 'dog' and 'animal' are incompatible. Property 'bark' is missing in type 'Animal' but required in type 'Dog'.
dogHandler
=
animalHandler
// 允许(正确逆变)

这种模式通过类型参数约束实现安全控制:

ts
interface 
Comparator
<
T
> {
compare
(
a
:
T
,
b
:
T
): number
} const
animalComparator
:
Comparator
<
Animal
> = { /*...*/ }
const
dogComparator
:
Comparator
<
Dog
> =
animalComparator
// 需要逆变支持

4. 类型参数控制

通过 inout 关键字实现显式变型控制(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
>) {
// 类型安全的事件处理 }

深入理解这些类型关系机制,可以帮助开发者:

  1. 设计更安全的泛型接口
  2. 合理配置编译器选项
  3. 优化复杂类型定义
  4. 预防潜在的类型安全漏洞
  5. 提升类型推导的准确性

实际开发中建议结合 strict 模式,通过类型测试验证关键类型的变型行为,在灵活性和安全性之间取得平衡。