ts逆变与协变
子类型
在讲逆变协变之前,先讲讲子类型。在类型系统中,如果类型 B 可以安全地用在任何需要类型 A 的地方,我们就说 B 是 A 的子类型。
例如:"hello" 是 string 的子类型,string 是 string | number 的子类型。
按照逻辑理解上来说,类型更加具体,包含了一个”宽泛“类型的所有”特征‘。那么这个更加具体的类型,就是这个“宽泛”类型的子类型。
即判断规则通常基于结构:B 拥有 A 的所有属性且类型兼容。
class Animal {
name: string
constructor(name: string) {
this.name = name
}
}
class Dog extends Animal {
bark() {
console.log('Woof!')
}
}
在上面代码中,Dog有Animal的所有类型,并且自己拓展了bark,所以我们可以说Dog是Animal的子类型。
那么就可以适用子类型规则,在需要使用Animal地方,通常都可以放一个Dog
let a: Animal
let d: Dog = new Dog()
a = d // ok
let c: Dog
let b: Animal = new Animal('Buddy')
c = b // 报错
更加复杂的情况-协变
基于上面的情况,我们拓展下,比如在泛型中。如果我们有一个泛型Handle<T>。我们是否也可以认为,Handle<Dog> 也是 Handle<Animal>的子类型?是的,可以这样认为,他和子类型的情况是一致的、协调的。称之为协变
class Animal {
name: string
constructor(name: string) {
this.name = name
}
}
class Dog extends Animal {
bark() {
console.log('Woof!')
}
}
interface ReadonlyBox<T> {
get(): T
}
let dogBox: ReadonlyBox<Dog> = {
get() {
return new Dog('Buddy')
}
}
let animalBox: ReadonlyBox<Animal>
animalBox = dogBox
逆变
逆变就是方向反过来。如果Dog是Animal的子类型。那么反过来Handler<Animal>反而是Handler<Dog>的子类型。
type Handler<T> = (value: T) => void
let handleAnimal: Handler<Animal> = (a) => {
console.log(a.name)
}
let handleDog: Handler<Dog>
handleDog = handleAnimal // 可以
上面的代码,其实逻辑上也是可以理解的
handleDog的要求是一个能处理Dog的函数
handleAnimal能处理所有的Animal,Dog是Animal的子类型,包含Animal的所有“特征”,自然也能处理dog。所以把handleAnimal赋值给handleDog没有问题。
但是根据子类型的定义,反过来Handler<Animal>反而是Handler<Dog>的子类型了。这样的情况就是称之为逆变。
如果还用协变的逻辑赋值
type Handler<T> = (value: T) => void
let onlyDog: Handler<Dog> = (dog) => {
dog.bark()
}
let onlyAnimal: Handler<Animal> = onlyDog // 不可以
onlyAnimal 只能处理Animal, 但是如果根据协变规则,传入onlyDog函数。如果传入一个没有dog.bark的Animal,这样就不安全了。
逆变协变的规律
在上面逆变、协变的的描述中。我们可以总结出一个规律:如果在泛型中,类型参数T,处于返回值位置中,应用协变规则;如果T处于参数位置中,应用逆变规则。
- 返回值位置:如果A是B的子类型,那么() => A 可以赋值给 () => B (需要返回B,但是我返回一个更加具体的子类型A,没有问题),协变。
- 参数位置:如果A是B的子类型,那么(B) => void 可以赋值给 (A) => void(需要处理A类型的参数, 但是我给你一个能处理更加宽泛的B的函数,依然可以安全的处理A),逆变
既处于返回值、又处于参数的位置-不变
interface Box<T> {
get(): T
set(value: T): void
}
let dogBox: Box<Dog> = {
get() { return new Dog(); },
set(value: Dog) {}
};
let animalBox: Box<Animal> = dogBox; // ❌ 错误:Type 'Box<Dog>' is not assignable to type 'Box<Animal>'
let animalBox2: Box<Animal> = {
get() { return new Animal(); },
set(value: Animal) {}
};
let dogBox2: Box<Dog> = animalBox2; // ❌ 错误:同样不兼容
上面代码的情况get是返回值位置,应用协变;set是参数位置,应用逆变。两种冲突了,所以保持了严格一致,也就是不变。虽然Dog是Animal的子类型。但是Box<Dog>和Box<Animal>没有任何关系,不能替代。
双变
双变指的是:如果A是B的子类型,那么(B) =>void 可以赋值给(A) => void(这是逆变),但是(A) => void也可以赋值给(B) => void(协变)。理论上来说函数参数应该遵守逆变才对。
但是Typescript不只是停留在理论上的产物,还要考虑到实用和兼容性。
JavaScript / DOM / 事件回调风格代码里,很多 API 都默认允许“写一个更具体一点的回调参数类型”。
如果 TypeScript 把所有地方都严格按逆变来查,很多旧代码、常见回调写法都会报错,开发体验会变差很多
所以在某些”方法参数“的位置,它不会严格按照逆变检查,而是采用更宽松的双变策略。
interface Handler {
// 方法签名:handle 的参数是 Event
handle(event: Event): void;
}
// 协变方向(子类型参数)—— 不安全但双变允许 ✅
const mouseHandler: Handler = {
handle(e: MouseEvent) {
console.log(e.clientX);
}
}
// 逆变方向(父类型参数)—— 安全,双变也允许 ✅
const anyHandler: Handler = {
handle(e: UIEvent) {
console.log(e.view);
}
};
评论 (0)
为了减少垃圾评论,请先使用 GitHub 登录后再发表评论。
使用 GitHub 登录评论暂无评论,成为第一个评论者!