协变与逆变
协变(Covariance)和逆变(Contravariance)描述的是:当类型之间存在子类型关系时,包含这些类型的复合类型(如函数、数组)的子类型关系如何变化。
这是理解函数类型兼容性报错的关键概念。
子类型是什么
先建立一个贯穿全文的例子。GIS 里的地理要素有明确的继承关系:
class Geometry {
wkt: string = '' // Well-Known Text 表示
}
class Point extends Geometry {
x: number = 0
y: number = 0
}
class LineString extends Geometry {
points: Point[] = []
length(): number { return 0 }
}Point 是 Geometry 的子类型。这意味着任何需要 Geometry 的地方,传入 Point 都是安全的(里氏替换原则)。
Point <: Geometry (读作:Point 是 Geometry 的子类型)协变(Covariant):方向一致
函数返回值是协变的:子类型关系与原始类型方向一致。
Point <: Geometry
↓ 方向一致
() => Point <: () => Geometry直觉上很好理解:一个"返回具体点位"的函数,可以放在"返回任意几何体"的位置用——调用方只需要一个 Geometry,你给了一个更具体的 Point,完全没问题。
type GetGeometry = () => Geometry
type GetPoint = () => Point
// ✅ 协变:可以把 GetPoint 赋给 GetGeometry
const getPoint: GetPoint = () => new Point()
const getGeometry: GetGeometry = getPoint数组也是协变的(在 TS 中):
const points: Point[] = [new Point()]
const geometries: Geometry[] = points // ✅ Point[] 可赋给 Geometry[]GIS 场景
渲染引擎接受一批几何体并渲染:
function renderAll(items: Geometry[]): void {
items.forEach(g => console.log(g.wkt))
}
const pointLayer: Point[] = [new Point(), new Point()]
renderAll(pointLayer) // ✅ 协变,Point[] 兼容 Geometry[]逆变(Contravariant):方向相反
函数参数是逆变的:子类型关系与原始类型方向相反。
Point <: Geometry
↓ 方向相反
(g: Geometry) => void <: (p: Point) => void这里比较反直觉,需要从"安全性"角度思考:
(p: Point) => void:调用时会传入一个Point,函数内部可能用到p.x、p.y(g: Geometry) => void:调用时会传入任意Geometry,函数内部只能用g.wkt
如果把一个"接受 Geometry"的函数放在"接受 Point"的位置,调用方传来 Point,函数只访问了通用属性,不会出问题。
反过来不行:把"接受 Point"的函数放在"接受 Geometry"的位置,调用方可能传来 LineString,而函数内部尝试访问 p.x,就崩了。
type HandleGeometry = (g: Geometry) => void
type HandlePoint = (p: Point) => void
// ✅ 逆变:可以把 HandleGeometry 赋给 HandlePoint
const handleGeometry: HandleGeometry = (g) => console.log(g.wkt)
const handlePoint: HandlePoint = handleGeometry
// ❌ 反方向不行
const handlePointOnly: HandlePoint = (p) => console.log(p.x, p.y)
const handleGeometryWrong: HandleGeometry = handlePointOnly // Error(开启 strictFunctionTypes 后)GIS 场景
地图事件监听器的类型兼容性:
type ClickHandler<T extends Geometry> = (feature: T) => void
// 注册点击事件,期望一个处理 Point 的回调
function onPointClick(handler: ClickHandler<Point>) {
handler(new Point())
}
// 一个能处理任意 Geometry 的通用回调
const logGeometry: ClickHandler<Geometry> = (g) => console.log(g.wkt)
// ✅ 逆变:处理 Geometry 的函数,可以安全地处理 Point
onPointClick(logGeometry)双变(Bivariant)
TS 在默认配置下,方法(method syntax)的参数是双变的——既可以协变也可以逆变,即宽松的兼容检查。
开启 strictFunctionTypes: true(strict: true 已包含)后,函数类型字面量((p: T) => void 形式)的参数会严格按逆变处理,但 method 语法(method(p: T): void)仍保持双变。
interface Processor {
// method 语法:双变(宽松)
processMethod(g: Geometry): void
// 属性函数语法:严格逆变(开启 strictFunctionTypes 后)
processFunc: (g: Geometry) => void
}不变(Invariant)
如果一个类型在某个位置既不能协变也不能逆变,则称为不变。泛型类型在读写都允许的场景下通常是不变的。
// 假设有一个可读写的容器
interface Box<T> {
get(): T // 返回值位置:协变
set(val: T): void // 参数位置:逆变
}
// Box<Point> 既不是 Box<Geometry> 的子类型,也不是父类型
// 因为同时有协变和逆变的需求,两个方向都无法兼容总结
| 位置 | 变性 | 子类型方向 |
|---|---|---|
| 函数返回值 | 协变 | 与原始类型一致 |
| 函数参数 | 逆变(严格模式) | 与原始类型相反 |
| 可读写容器 | 不变 | 两个方向都不兼容 |
| method 语法参数 | 双变 | 两个方向都兼容 |
记忆口诀:产出(return)协变,消费(param)逆变。产出时给更具体的没问题,消费时需要能处理更宽泛的才安全。

