介绍递归 `Pipe` 和 `Compose` 类型
介绍
创建递归管道类型
实现管道功能
撰写
结束语
事实证明,递归Pipe
(和Compose
)类型比传统的参数重载方法具有关键优势。关键优势如下:
- 保留变量名称
- 函数签名中对泛型的容忍度更高
- 可变参数入口函数
- 理论上可以组成无限的函数
在本文中,我们将探讨如何构建这种Pipe
和类型。Compose
如果您想要了解手头的完整源代码,请参阅此 repo:
https://github.com/babakness/pipe-and-compose-types
介绍
我的这个项目之旅始于一个挑战:创建递归Pipe
和Compose
类型,而不依赖于重命名参数的重载。要查看如何使用重载构建此示例,请点击以下链接之一,访问@gcantifp-ts
的优秀库。
管道:
https://github.com/gcanti/fp-ts/blob/master/src/function.ts#L222
撰写:
https://github.com/gcanti/fp-ts/blob/master/src/function.ts#L160
我在自己的项目中使用过这种策略。参数名称会丢失,并被替换,在本例中,会使用字母名称,例如a
和b
。
TypeScript 与 JavaScript 共享生态系统。由于 JavaScript 缺乏类型,参数名称在指导使用方面尤其有用。
让我们更深入地了解一下这些类型是如何工作的。
创建递归管道类型
首先,我们需要一些辅助类型。它们将从通用函数中提取信息:
export type ExtractFunctionArguments<Fn> = Fn extends ( ...args: infer P ) => any ? P : never
export type ExtractFunctionReturnValue<Fn> = Fn extends ( ...args: any[] ) => infer P ? P : never
接下来是另外两个助手,一个简单的类型,允许我们根据测试类型分支不同的类型,以及一个用于表达任何功能的简写。
type BooleanSwitch<Test, T = true, F = false> = Test extends true ? T : F
export type AnyFunction = ( ...args: any[] ) => any
下一种类型确实很深奥且特殊:
type Arbitrary = 'It was 1554792354 seconds since Jan 01, 1970 when I wrote this'
type IsAny<O, T = true, F = false> = Arbitrary extends O
? any extends O
? T
: F
: F
本质上,此类型检测any
和unknown
。它会对 感到困惑{}
。无论如何,它不会被导出,仅供内部使用。
有了这些帮助器,Pipe 的类型如下:
type Pipe<Fns extends any[], IsPipe = true, PreviousFunction = void, InitalParams extends any[] = any[], ReturnType = any> = {
'next': ( ( ..._: Fns ) => any ) extends ( ( _: infer First, ..._1: infer Next ) => any )
? PreviousFunction extends void
? Pipe<Next, IsPipe, First, ExtractFunctionArguments<First>, ExtractFunctionReturnValue<First> >
: ReturnType extends ExtractFunctionArguments<First>[0]
? Pipe<Next, IsPipe, First, InitalParams, ExtractFunctionReturnValue<First> >
: IsAny<ReturnType> extends true
? Pipe<Next, IsPipe, First, InitalParams, ExtractFunctionReturnValue<First> >
: {
ERROR: ['Return type ', ReturnType , 'does comply with the input of', ExtractFunctionArguments<First>[0]],
POSITION: ['Position of problem for input arguments is at', Fns['length'], 'from the', BooleanSwitch<IsPipe, 'end', 'beginning'> , 'and the output of function to the ', BooleanSwitch<IsPipe, 'left', 'right'>],
}
: never
'done': ( ...args: InitalParams ) => ReturnType,
}[
Fns extends []
? 'done'
: 'next'
]
此类型需要经过一系列步骤,首先迭代每个函数,从头部开始,然后递归地将尾部传递到下一个迭代。实现此操作的关键是使用以下技术从函数数组中提取并分离出第一个元素:
( ( ..._: Fns ) => any ) extends ( ( _: infer First, ..._1: infer Next ) => any )
如果我们不做错误检查,我们可以将下一部分简化为
PreviousFunction extends void
? Pipe<Next, IsPipe, First, ExtractFunctionArguments<First>, ExtractFunctionReturnValue<First> >
: Pipe<Next, IsPipe, First, InitalParams, ExtractFunctionReturnValue<First> >
PreviousFunction
仅在第一次迭代时为 void。在这种情况下,我们提取初始参数。InitialParams
在每次迭代中,我们将最后一个函数的返回类型传回。一旦我们用完列表中的所有函数,这部分
Fns extends []
? 'done'
: 'next'
返回done
,我们可以返回一个由初始参数和最后一个返回类型组成的新函数
'done': ( ...args: InitalParams ) => ReturnType,
其他部分是错误检测。如果检测到错误,它将返回一个自定义对象,该对象指向发生错误的计数。换句话说,它具有内置的错误报告功能。
我在研究其他人的图书馆时学到了这项技术。一个值得注意的例子是typescript-tuple
我们稍后用来构建Compose
好的,现在让我们为管道函数本身创建一个别名
type PipeFn = <Fns extends [AnyFunction, ...AnyFunction[]] >(
...fns: Fns &
Pipe<Fns> extends AnyFunction
? Fns
: never
) => Pipe<Fns>
这里还有另一种技巧可以说明。当Pipe
函数返回那个有用的错误对象时,我们实际上也希望引发编译器错误。我们通过将匹配的类型fns
与自身或条件连接来实现这一点never
。后者的条件会导致错误。
最后,我们准备定义管道。
我在不同的项目中执行此操作,而不仅仅是在同一个项目的不同文件中执行此操作。我这样做有两个原因:
首先,我想将实现与类型分离。您可以自由使用这些类型,而无需包含任何 JavaScript。
其次,一旦类型正确编译,我想将未来 TypeScript 版本的优缺点与类型和实现分开。
实现管道功能
export const pipe: PipeFn = ( entry: AnyFunction, ...funcs: Function1[] ) => (
( ...arg: unknown[] ) => funcs.reduce(
( acc, item ) => item.call( item, acc ), entry( ...arg )
)
)
让我们看看它是如何工作的:
const average = pipe(
( xs: number[]) => ( [sum(xs), xs.length] ),
( [ total, length ] ) => total / length
)
✅ 我们看到平均值具有正确的类型(xs: number[]) => string
并且参数名称被保留。
让我们尝试另一个例子:
const intersparse = pipe(
( text: string, value: string ): [string[], string] => ([ text.split(''), value ]),
( [chars, value]: [ string[], string ] ) => chars.join( value )
)
✅ 两个参数名称均被保留(text: string, value: string) => string
让我们尝试一个可变参数的例子:
const longerWord = ( word1: string, word2: string ) => (
word1.length > word2.length
? word1
: word2
)
const longestWord = ( word: string, ...words: string[]) => (
[word,...words].reduce( longerWord, '' )
)
const length = ( xs: string | unknown[] ) => xs.length
const longestWordLength = pipe(
longestWord,
length,
)
✅ 参数名称和类型检查,其类型longestNameLength
为(word: string, ...words: string[]) => number
伟大的!
撰写
事实证明,我们可以Compose
非常轻松地做到这一点。我们需要的帮助程序来自typescript-tuple
。
import { Reverse } from 'typescript-tuple'
export type Compose<Fns extends any[]> = Pipe<Reverse<Fns>, false>
export type ComposeFn = <Fns extends [AnyFunction, ...AnyFunction[]] >(
...fns: Fns &
Compose<Fns> extends AnyFunction
? Fns
: never
) => Compose<Fns>
实现方式仅略有不同
import { ComposeFn } from 'pipe-and-compose-types'
export const compose: ComposeFn = ( first: Function1, ...funcs: AnyFunction[] ): any => {
/* `any` is used as return type because on compile error we present an object,
which will not match this */
return ( ...arg: unknown[] ) => init( [first, ...funcs] ).reduceRight(
(acc, item) => item.call( item, acc ), last(funcs)( ...arg )
)
}
让我们来compose
测试一下这个新功能:
const longestWordComposeEdition = compose(
length,
longestWord,
)
✅ 参数名称和类型检查,其类型longestNameLength
为(word: string, ...words: string[]) => number
结束语
我鼓励你看一下这个 repo 来回顾一下类型
https://github.com/babakness/pipe-and-compose-types
要将类型导入您自己的项目,请使用以下方式安装:
npm install pipe-and-compose-types
另请参阅这些类型的两个重要应用
https://github.com/babakness/pipe-and-compose
使用以下方法将这些函数导入到您的项目中
npm install pipe-and-compose
请分享你的想法!也欢迎在Twitter上联系我!
文章来源:https://dev.to/hemaka/introducing-the-recursive-pipe-and-compose-types-3g9o