TypeScript 中的协变与逆变
TypeScript 中协变与逆变
“协变(Covariance)”和“逆变(Contravariance)”是类型系统中关于**子类型关系(subtyping)**的一个核心概念,主要应用在函数类型的参数与返回值、泛型类型的继承、接口之间的赋值兼容性等场景中。
此外除了协变与逆变,还有双变和不变,不变为必须完全一致, 很好理解,而双变在严格模式中并不支持,没有了解意义。
什么是协变
用通俗的话来说协变指的是:更模糊的类型可以适配更具体的类型,例如类型中的父类(更模糊的类型)和子类(更具体的类型),给父类类型指定一个子类类型是被允许的,这就是协变。
举个例子:
// 父类型
interface Animal {name: string;
}
// 子类型
interface Dog extends Animal {type: string;
}let animal: Animal = {name: '动物',
};
let dog: Dog = {name: '修狗',type: 'dog',
};animal = dog; // ✅ 将更具体的类型赋予给更广泛的类型,是允许的
dog = animal; // ❌ 将更广泛的类型断言为更具体的类型,是不允许的
我们声明了两个类型 Animal
和 Dog
,两个类型属于父子类型,当我们将 Dog
类型的数据赋予给 Animal
类型的数据时,是允许的。毕竟 Dog
属于 Animal
,毕竟狗也属于动物,反过来将Animal
类型的数据赋予给 Dog
类型的数据时,是不被允许的,因为动物不一定是狗。这很符合直觉。
也就是说协变通俗来说就是:要求更模糊的数据类型但是允许赋予更具体的数据类型。
函数中的协变和逆变
先说结论,在函数中,函数的返回值是协变的,和上述所说的直接赋值一样,属于符合直觉的。但是函数的参数是逆变的。
返回值的协变
返回值的协变和上面说的差不多,简单举个例子:
interface Animal {name: string;
}
interface Dog extends Animal {type: string;
}let animal: Animal = {name: '动物',
};
let dog: Dog = {name: '修狗',type: 'dog',
};type AnimalFn = () => Animal;
type DogFn = () => Dog;let animalFn: AnimalFn = () => dog; // ✅ 要求的返回值是 Animal 但是返回 Dog 是允许的
let dogFn: DogFn = () => animal // ❌ 要求的返回值是 Dog 但是返回 Animal 是不允许的
AnimalFn
类型要求返回更模糊的 Animal
类型,但是返回更具体的 Dog
类型却是被允许的,这个是协变。
参数中的逆变
前面说了协变是:要求更模糊的数据类型但是允许赋予更具体的数据类型,那么反过来逆变就是:要求更具体的数据类型但是允许赋予更模糊的数据类型。
举个例子:
interface Animal {name: string;
}
interface Dog extends Animal {type: string;
}type AnimalFn = (arg: Animal) => void;
type DogFn = (arg: Dog) => void;let animalFn: AnimalFn = (arg: Dog) => {}; // ❌ 要求的参数是 Animal 但是传入 Dog 是不允许的
let dogFn: DogFn = (arg: Animal) => {} // ✅ 要求的参数是 Dog 但是传入 Animal 是允许的
从上面代码来看,animalFn
是 AnimalFn
类型的,要求一个 Animal
类型参数的函数,我们传入一个 Dog
类型,这属于给更模糊的类型传入一个更具体的类型,按照我们上面函数返回值协变的逻辑,应该是可以的,但是在函数参数这里是不被允许的,因为函数的参数是逆变的;
而在 dogFn
的定义中,DogFn
类型的 dogFn
事要求一个 Dog
类型的参数的,我们传入了一个 Animal
类型,这属于给更具体的类型传入更模糊的类型,这是被允许的,这就是逆变。
函数参数逆变的运用
利用函数参数逆变实现件联合类型转换成交叉类型
要求:
type A = { foo: string }
type B = { bar: number }type U = A | B;// 我们想把它变成:
type I = A & B;
实现:
type A = { foo: string }
type B = { bar: number }type UnionToIntersection<T> = (T extends any ? (arg: T) => any : never) extends (arg: infer R) => any? R: never;type P = UnionToIntersection<A | B>; // P = A & B
解析:
extends
、 infer
等类型推导的使用可以参考 TypeScript 这篇文章。
首先 T extends any ? (arg: T) => any : never
,我们假设传入的 T 是 A | B,那么 T extends any ? (arg: T) => any : never 推导出来的类型就是 (arg: A) => any | (arg: B) => any
;
然后我们利用 extends
用 (arg: infer R) => any
去匹配 (arg: A) => any | (arg: B) => any
,用 infer
将函数的参数类型提取出来命名为 R
。
因为 R
要兼容所有的函数的参数,而函数的参数是逆变的,也就是说实际定义的 A | B 的类型应该为更具体的类型,而 R
作为赋予 A | B
的类型应该为更模糊的类型,所以 R
需要为 A & B
才能同时作为 (arg: A) => any | (arg: B) => any
的参数类型,同时满足 A
和 B
两种类型。
最后我们将提取到的 R
返回,从而得到类型 A & B