TypeScript:一文搞懂 infer
不久前曾做了一场分享,希望可以用 md 记录下来,主要是想深入浅出地帮助大家搞懂 TypeScript 中最难的一部分,协变逆变和 infer 以及前置知识。
类型兼容性
研究 TypeScript 的关键问题就是讨论两个类型之间的兼容性,我们简单地把类型之间的关系分为三种:
- A 是 B 的子类型
- A 是 B 的超类型
- A 和 B 不兼容
需要注意的是,子类型与子类不同,仅描述两个类型之间的兼容性,且子类型是包括自身的,就像集合子集也包括自己一样。
后续为了方便描述,A -> B 表示 B 是 A 的子类型
研究它有什么用呢?
比如 A 是 B 的子类型,那么类型为 A 的变量就可以赋给类型为 B 的变量了
鸭子类型
鸭子类型是 TypeScript 类型的最大特性:仅关注对象上的属性和方法,而不关注继承关系
比如
Class Duck{ swim }
Class Dog{ swim,bark }
(伪代码)
在 TypeScript 中 Dog 是 Duck 的子类型,因为它满足了 Duck 上的所有方法和属性,而在 Java 中它们是不兼容的
也因为鸭子类型,我们可以使用集合概念去理解 TypeScript 类型之间的关系。
类型运算 「&」和「|」
&:求两个类型的并集,同名属性也对其类型 &
|:求两个类型的交集,只保留同名属性且也对其类型 |
两个类型运算有极为重要的关系:A | B -> A or B -> A & B
我们可以按类的继承来理解:子类不但有父类上的所有属性和方法,还有自己的属性和方法。
那么, 显然 A 有 A|B 上所有属性, A & B 和 A 同理。
函数子类型
协变和逆变
这两个词仅仅是很简单的定义,描述两个过程:
协变:类型推导到其子类型的过程,A | B -> A & B 就是一个协变
逆变:类型推导到其超类型的过程
如何判断函数子类型?
为了方便描述,「Dog => Dog」 表示「参数为 Dog,返回值为 Dog 的函数」
其他类型的子类型我们很好判断,函数的子类型却很难,比如有关系 Animal -> Dog -> Shepherd
你能一眼看出下面哪个是 Dog => Dog 子类型吗?
A、Animal => Shepherd B、Shepherd => Aniaml
答案是 A
我们如果把视角划分,Dog => Dog 作为
参数的传入者:只能保证传入 Dog 参数,所以当我们定义参数为 Animal 时,只能使用 Animal 上的属性和方法,而 Dog 肯定有,就能保证类型的正确。
使用返回值者:保证只使用 Dog 方法,所以当我们定义返回值为 Shepherd,使用者只使用 Dog 上的属性和方法,而 Shepherd 肯定有,就能保证类型的正确。
所以,Dog => Dog -> Animal => Shepherd
也称 参数是逆变的,返回值是协变的。
至于你为什么这么少看到协变和逆变的概念,只因为 TypeScript 只有一处逆变,就是参数
infer
infer 的作用一言蔽之:推导泛型参数
看一个 infer 例子
type numberPromise = Promise<number>;
type n = numberPromise extends Promise<infer P> ? P : never; // number
在 Promise
输入了 number
获得一个新的类型,那么 infer 就可以通过已知的类型和获得它泛型反推出泛型参数
从返回值得到参数?在 JS 似乎很难想象,但是在 TypeScript 中 infer 就是用于做这个事情,不过注意它仅仅是推导,而非映射,遵循着一套规则,下文会具体讲解。
还有注意一点,infer 只能在 extends 的右边使用,infer P 的 P 也只能在条件类型为 True 的一边使用,下文会讲解这个限制的意义。
推导过程
以下代码为例
type getIntersection<T> = T extends (a: infer P,b: infer P) => void ? P : never;
type Intersection = getIntersection<(a: string, b: number)=> void> // string & number
- infer 必须在 extends 右侧使用,因为必须保证这个已知类型是由右侧的泛型推出来的,不然推导它的参数还有什么意义呢? 检查时会跳过使用了 infer 的地方。
- 遵循以下规则推导 P,有四种情况:
- P 只在一个位置占位:直接推出类型
- P 都在协变位置占位:推出占位类型的联合
- P 都在逆变位置占位:推出占位类型的交叉(目前只有参数是逆变)
- P 既在顺变位置又在逆变位置:只有占位类型相同才能使 extends 为 true,且推出这个占位类型
我们的例子属于第三种情况,P 都在逆变位置占位,最终就推出两个类型的交叉 string & number
那么为何是这种关系,可以朴素地解释一下:
首先回顾下类型运算关系和函数子类型:A | B -> A or B -> A & B,函数子类型的参数是逆变的
因为 (a: string, b: number)=> void extends (a: infer P,b: infer P) => void
,所以(a: string, b: number)=> void
是 (a: infer P,b: infer P) => void
子类型,所以 P 到 string 或者 number 是逆变,然而我们这里是反过来推 P,所以 string 或 number 到 P 是协变,最终就推出 string & numner
当 P 只在一个位置占位时,它推出来的类型就是一一对应的,比如 Parameter
和 ReturnType
小结
本文从三个方面渐进地讲解 infer 的知识:
类型兼容性,理解 A | B -> A or B -> A & B 的关系;
函数子类型以及协变逆变,因为只有参数是逆变的,所以需要理解函数子类型参数为何是逆变的;
最后讲解 infer 的作用,推导泛型参数,以及 infer 的推导规则,单处直推,多处协变推出联合,多处逆变推出交叉。
转载自:https://juejin.cn/post/6998347146709696519