TypeScript 中的逆变、协变、双变与不变
在 TypeScript 的类型系统中,逆变(Contravariance)、协变(Covariance)、双变(Bivariance) 和 不变(Invariance) 是描述类型兼容性和子类型关系的重要概念。这些术语来源于类型理论,主要用于处理函数类型、泛型和接口的赋值规则。理解这些概念不仅能帮助我们编写更安全的代码,还能让我们更好地设计类型系统。本文将逐一讲解这四种变型,辅以代码示例,让你轻松掌握它们的含义与应用。
协变(Covariance):返回值类型的“向上兼容”
定义与原理
协变是指如果类型 A
是 B
的子类型(记作 A <: B
),那么 T<A>
也是 T<B>
的子类型。简单来说,子类型可以安全地“向上”转换为父类型。
应用场景
协变最常见于函数的返回值类型。在 TypeScript 中,函数返回的类型如果是子类型,则可以赋值给期待父类型的地方。
1 | class Animal {} |
这里,Dog
是 Animal
的子类型,getDog
返回 Dog
,而 getAnimal
期待返回 Animal
。由于返回值类型是协变的,这种赋值是合法的。
为什么安全?
调用 getAnimal()
时,返回的是 Dog
,但调用者只关心 Animal
的属性和方法。由于 Dog
继承了 Animal
,它完全满足要求,因此协变在返回值中是安全的。
逆变(Contravariance):参数类型的“向下兼容”
定义与原理
逆变是指如果 A
是 B
的子类型(A <: B
),那么 T<B>
是 T<A>
的子类型。与协变相反,逆变的方向是从父类型到子类型。
应用场景
逆变通常出现在函数参数类型中。在 TypeScript 的严格模式(strictFunctionTypes: true
)下,函数参数默认是逆变的,即接受父类型的函数可以赋值给期待子类型的地方。
1 | class Animal {} |
handleAnimal
接受 Animal
类型参数,而 handleDog
期待 Dog
类型。由于 Dog
是 Animal
的子类型,这种赋值在逆变规则下是合法的。
为什么安全?
调用 handleDog(new Dog())
时,传入的是 Dog
,而 handleAnimal
能处理任何 Animal
(包括 Dog
)。逻辑上不会出错,因此逆变在参数类型中是安全的。
双变(Bivariance):宽松的双向兼容
定义与原理
双变是指类型既支持协变又支持逆变,即在某些情况下,类型可以向上或向下兼容。这种行为通常出现在 TypeScript 的非严格模式(strictFunctionTypes: false
)中。
应用场景
双变主要影响函数参数类型。当严格模式关闭时,TypeScript 允许参数类型双向赋值。
1 | class Animal {} |
在非严格模式下,fn1
(要求 Dog
)可以赋值给 fn2
(要求 Animal
),反过来 fn3
(要求 Animal
)也能赋值给 fn4
(要求 Dog
)。
注意事项
双变虽然灵活,但可能导致类型不安全。例如,fn2
可能传入 Animal
而非 Dog
,而 fn1
无法处理这种情况。因此,严格模式下 TypeScript 只支持参数的逆变,以确保类型安全。
不变(Invariance):严格的类型匹配
定义与原理
不变是指类型 T<A>
和 T<B>
之间没有任何兼容性,即使 A
和 B
存在子类型关系。只有当类型完全相同时才兼容。
应用场景
不变通常出现在泛型类型同时作为输入(逆变)和输出(协变)时。由于两种变型要求冲突,TypeScript 要求类型保持不变。
1 | interface Container<T> { |
dogContainer
的setValue
只接受Dog
,而animalContainer
的setValue
要求接受Animal
,这违反了逆变规则。- 反过来,
animalContainer
的value
返回Animal
,而dogContainer
期待 Dog,这违反了协变规则。 因此,Container<Dog>
和Container<Animal>
互不兼容,表现出不变性。
总结与对比
变型 | 定义 | 典型场景 | 示例兼容性 |
---|---|---|---|
协变 | A <: B -> T<A> <: T<B> |
返回值类型 | ()=>Dog -> () => Animal |
逆变 | A <: B -> T<B> <: T<A> |
参数类型 | (Animal) => void -> (Dog) => void |
双变 | 既协变又逆变 | 非严格模式参数 | 双向兼容(不安全) |
不变 | T<A> 和 T<B> 无关 |
输入+输出 | Container<Dog> != Container<Animal> |
实际意义
- 协变与逆变:分别适用于返回值和参数的类型检查,是类型系统灵活性的体现。
- 双变:非严格模式下的妥协,牺牲了安全性换取兼容性。
- 不变:在复杂场景下强制类型一致,确保程序的正确性。
通过理解这些变型规则,我们可以更好地设计 TypeScript 的接口和泛型,避免类型错误。例如,在严格模式下优先考虑逆变的参数规则,或在泛型设计中明确类型的使用位置(输入还是输出)。