什么是管道函数
管道函数允许多个函数组合在一起,并将函数的返回值交予下一个函数的输入执行。直到最后一个函数。
例如:
const add = value => value + 1 // 将输入的值加一
const numberToBoolean = value => Boolean(value) // 将一个值转换成布尔值
const isBooleanTrue = value => value === true // 判断是否为true
// 例如我们有上面上个函数我们依次执行并将返回的结果赋值给下一个函数
const result = isBooleanTrue(numberToBoolean(add(1))) // true
很显然这样的书写方式并不很直观,我们需要实现一个管道函数,可以将需要执行的结果作为参数传入,并返回一个新的函数。
/**
* 函数管道
* @param agrs (...agrs: any[]) => any
* @returns Pipe
*/
export const pipe = (...agrs)=> {
return agrs.reduce((callback, next) => (...values) => next(callback(...values)))
}
管道函数的实现比较简单,我这里使用reduce实现,也可以使用递归来方便理解。
/**
* 函数管道 递归写法
* @param agrs (...agrs: any[]) => any
*/
export const pipeDeep = (...agrs) => {
if (agrs.length === 1) return agrs[0]
else {
return (...values) => {
const [first, ...rest] = agrs
return pipeDeep(...rest)(first(...values))
}
}
}
理解了函数管道之后,我们重点来讨论下如何实现对其进行TS类型标注
类型标注
首先我们知道管道函数的几个重点
- 返回值是一个函数
- 返回函数的参数为第一个参数的参数
- 返回函数的返回值为最后一个参数的返回值
- 每一个函数参数的参数应该都是上一个参数的返回值(这里比较绕,可以多理解下)
- 例如 add 函数 和 numberToBoolean 为函数管道的前两个参数,那add的参数和无需考虑,但是 numberToBoolean 的参数需要和add的返回值保持一致在可以保证类型的准确性
- 我们这里只考虑单一参数的情形,因为多参的话,我们无法判断下一个参数是需要当作数组使用还是多参使用
我们来一步一步写
第一步返回一个函数(比较简单我们一笔带过)
type Pipe = (...args: any[]) => any
第二步 返回函数的参数为第一个函数的参数
type F = (...agrs: any[]) => any
type InferParameters<T> = T extends (...args: infer R) => any ? R : any;
type Pipe <T extends F[]> = T extends [infer A, ...infer Res] ? (...agrs: InferParameters<A>) => any : never
在这里我们通过infer 关键字来提取出第一个函数的参数,并作为返回函数的参数
第三步 返回函数的返回值为最后一个参数的返回值
type F = (...agrs: any[]) => any
type InferParameters<T> = T extends (...args: infer R) => any ? R : any;
type InferReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type Pipe <T extends F[]> = T extends [infer A, ...infer Res, infer E] ? (...agrs: InferParameters<A>) => InferReturnType<E> : never
第三步和第二步几乎相同,我们用infer关键字拿出最后一项的返回值
但是我估计大家应该也看出问题了这种写法一定要在参数大于两项时才会生效。如果参数只有一项,那应该直接返回当前传入的函数。
那么接下来我们将这个判断和第四步一起融合
type F = (...agrs: any[]) => any
type InferReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type Pipe <T extends F[]> =
T extends [infer A]
? A : T extends [(...agrs: infer A) => infer R, ...infer Res]
? Res extends [infer B]
? B extends (...agrs: R[]) => any
? (...agrs: A) => ReturnType<B> : never : Res extends [infer B, ...infer Res2]
? B extends (...agrs: R[]) => any
? Pipe<[(...agrs: A) => InferReturnType<B>, ...(Res2 extends F[] ? Res2 : any)]> : never
: never
: never
我们通过infer关键字来判断参数的数量,如果只有一项直接返回,如果大于一项,将第一项的参数和第二项的结果生成新的函数在递归执行Pipe,并且判断了函数参数的是否为上一个参数的返回值。
是否有优化空间
对于这个函数,是我个人在空闲时间去思考的,深度有限,不知道是否有更加优雅的写法,如果有,欢迎大家反馈给我。