2解决通用返回结果是泛型_Typescript复杂泛型实践:如何切掉函数参数表的最后一个参数?...

21d7fd4871ecaccac837cea400d278da.png

最近有个同事问了我个问题:

如何编写一个ts泛型工具Transfer<Fn>,将Fn的参数表最后一个参数切掉,并返回切掉参数之后的函数类型?

function inputFn(a: number, b: string, c: boolean) {
  return a
}

type OutputFn = Transfer<typeof inputFn> // 希望返回类型 (a: number, b: string) => number

以上暂不考虑(a: number, ...rest: any[]) => void这种情况。

其实实际项目中,我们是尽量不用这种思路编写代码的,一方面inputFn的类型大多数时候我们是知道的,要转换类型直接手写一个新类型 as 一下就行了,没有必要做通用工具;另一方面,复杂的ts泛型代码,是难以维护的。

但是作为一个思考题,还是挺有意思,所以我就研究了一下这个问题。

工具

就好像生产汽车一样,为了实现复杂的泛型需求,我们需要一些工具,通过组合这些工具,我们可以逐渐逼近复杂需求。这是解决复杂问题的一贯思路。

那么ts中出了基本的关键字和内置泛型工具(比如ReturnType),还有哪些可以帮助我们呢?首先我们罗列一下跟本次主题相关的工具。

三元运算符 + infer,实现类型萃取

这是大家常用的一个技术手段,通过三元运算符,将工作场景框定到我们预设的场景中,再通过infer关键字,萃取出抽象场景实际落地时,某个类型信息的实际取值。

比如我们可以这样获得函数的参数表元组类型:

// 获取函数参数表的类型
type ArgumentType<F> = F extends (...args: infer U) => any ? U : never

利用函数参数表操作元组

ts到目前为止还存在一个问题,就是有些我们认为天经地义的操作,在元组身上实现不了。比如:

type P = [number, string, boolean]
type Q = Date

type R = [Q, ...P] // 报错

上面的代码,是想将一个元素类型添加到一个元组的头部,生成一个新的元组类型。这本来个是很自然的操作,但是到目前(3.8.3)为止,ts还不支持这种写法。

为了解决这个问题,我们可以把元组放入函数的参数表去操作:

type Prepend<Tuple extends any[], Addend> = ((_0: Addend, ..._1: Tuple) => any) extends ((..._: infer Result) => any) ? Result : never

如果要切掉元组的第一个元素,也可以用类似的手法实现:

type CutHead<Tuple extends any[]> = ((...args: Tuple) => any) extends (first: any, ...rest: infer Result) => any ? Result : never

递归声明

ts的类型声明支持递归,比如我们可以这样定义单向链表的节点类型:

type ListNode<T> = {
  data: T
  next: ListNode<T> | null
}

这看起来很简单,但是实际使用的时候,我们不仅要描述递归,还要描述何时停止递归。比如说,假设我们想通过两个数字字面量类型M和N,获得M+N这个字面量类型,目前ts还不支持字面量类型计算,但是 有人 实现了一个很有意思的toy版本:

type Inc = { [n: number]: number; 0: 1; 1: 2; 2: 3; 3: 4; }
type Dec = { [n: number]: number; 0: -1; 1: 0; 2: 1; 3: 2; }
type Matches<V, T> = V extends T ? '1' : '0'

type Add<A extends number, B extends number> = { 1: A, 0: Add<Inc[A], Dec[B]> }[Matches<B, 0>];

这个版本只支持0-3的加法,显然是不能用的,但是其中Add的实现,展示了一种让递归停止的技巧:

type Foobar<T> = {
  1: something, // 停止递归的分支
  0: Foobar<...> // 继续递归的分支
}[Condition<...>] // 一个返回字面量“1”或“0”的泛型工具

以上就是我们会用到的ts基础工具。

实现

首先,如果需求是“切掉函数参数表的第一个参数”,那么我们已经实现了(上面的CutHead稍作改动即可),但是如何切掉最后一个(就叫CutTail吧),我们还不知道。

那么,我们不妨尝试一下,通过CutHead去实现CutTail。如果我们有一个反转元组的工具Reverse,好像问题就变得很简单了——先把元组反转一下,切掉第一个,再反转回来,搞定:

type CutTail<Tuple extends any[]> = Reverse<CutHead<Reverse<Tuple>>>

看起来不错,那么如何实现Reverse呢?这就要用到我们刚才提到的递归声明,以及停止递归的模式,具体代码如下:

type Reverse<Tuple extends any[], Prefix extends any[] = []> = {
    0: Prefix
    1: ((..._: Tuple) => any) extends ((_0: infer First, ..._1: infer Next) => any)
        ? Reverse<Next, Prepend<Prefix, First>>
        : never
}[Tuple extends [any, ...any[]] ? 1 : 0]

这段代码写的很巧(对,我是抄的),通过(_0: infer First, ..._1: infer Next) => any 拿到第一个元素类型First,再通过Prepend<Prefix, First> 把第一个元素放在最后(尽管你每次都是prepend,但是下次还会prepend所以你这次prepend的元素就在后面了)。

注意Tuple extends [any, ...any[]]这个判断条件——尽管我们可以通过Tuple['length']拿到元组的长度,但是由于字面量无法参与运算,所以我们无法将元组长度作为停止递归的条件。 而Tuple extends [any, ...any[]]这个写法的语义是“长度大于0”,很高的充当了替代品。

至此,问题解决:

type Transfer<F extends (...args: any[]) => any> = (...args: CutTail<ArgumentType<F>>) => ReturnType<F>

后续

由于ts类型有些功能还不算完善,所以我们实现一些功能总要绕来绕去,想猜谜一样,甚至有些功能压根实现不出来。但是这样的猜谜练习,是有助于我们平时灵活使用ts的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值