介绍递归管道和组合类型简介创建递归管道类型实现管道函数组合关闭

2025-06-07

介绍递归 `Pipe` 和 `Compose` 类型

介绍

创建递归管道类型

实现管道功能

撰写

结束语

事实证明,递归Pipe(和Compose)类型比传统的参数重载方法具有关键优势。关键优势如下:

  • 保留变量名称
  • 函数签名中对泛型的容忍度更高
  • 可变参数入口函数
  • 理论上可以组成无限的函数

在本文中,我们将探讨如何构建这种Pipe和类型。Compose

如果您想要了解手头的完整源代码,请参阅此 repo:

https://github.com/babakness/pipe-and-compose-types

介绍

我的这个项目之旅始于一个挑战:创建递归PipeCompose类型,而不依赖于重命名参数的重载。要查看如何使用重载构建此示例,请点击以下链接之一,访问@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

我在自己的项目中使用过这种策略。参数名称会丢失,并被替换,在本例中,会使用字母名称,例如ab

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


Enter fullscreen mode Exit fullscreen mode

接下来是另外两个助手,一个简单的类型,允许我们根据测试类型分支不同的类型,以及一个用于表达任何功能的简写。



type BooleanSwitch<Test, T = true, F = false> = Test extends true ? T : F
export type AnyFunction = ( ...args: any[] ) => any


Enter fullscreen mode Exit fullscreen mode

下一种类型确实很深奥且特殊:




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


Enter fullscreen mode Exit fullscreen mode

本质上,此类型检测anyunknown。它会对 感到困惑{}。无论如何,它不会被导出,仅供内部使用。

有了这些帮助器,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'
]


Enter fullscreen mode Exit fullscreen mode

此类型需要经过一系列步骤,首先迭代每个函数,从头部开始,然后递归地将尾部传递到下一个迭代。实现此操作的关键是使用以下技术从函数数组中提取并分离出第一个元素:



( ( ..._: Fns ) => any ) extends ( ( _: infer First, ..._1: infer Next ) => any )


Enter fullscreen mode Exit fullscreen mode

如果我们不做错误检查,我们可以将下一部分简化为



PreviousFunction extends void
        ? Pipe<Next, IsPipe, First, ExtractFunctionArguments<First>, ExtractFunctionReturnValue<First> >
        : Pipe<Next, IsPipe, First, InitalParams, ExtractFunctionReturnValue<First> >


Enter fullscreen mode Exit fullscreen mode

PreviousFunction仅在第一次迭代时为 void。在这种情况下,我们提取初始参数。InitialParams在每次迭代中,我们将最后一个函数的返回类型传回。一旦我们用完列表中的所有函数,这部分



 Fns extends []
    ? 'done'
    : 'next'


Enter fullscreen mode Exit fullscreen mode

返回done,我们可以返回一个由初始参数和最后一个返回类型组成的新函数



'done': ( ...args: InitalParams ) => ReturnType,


Enter fullscreen mode Exit fullscreen mode

其他部分是错误检测。如果检测到错误,它将返回一个自定义对象,该对象指向发生错误的计数。换句话说,它具有内置的错误报告功能。

我在研究其他人的图书馆时学到了这项技术。一个值得注意的例子是typescript-tuple我们稍后用来构建Compose

好的,现在让我们为管道函数本身创建一个别名



type PipeFn = <Fns extends [AnyFunction, ...AnyFunction[]] >( 
  ...fns: Fns & 
    Pipe<Fns> extends AnyFunction 
      ? Fns 
      : never 
) =>  Pipe<Fns>


Enter fullscreen mode Exit fullscreen mode

这里还有另一种技巧可以说明。当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 ) 
  ) 
) 


Enter fullscreen mode Exit fullscreen mode

让我们看看它是如何工作的:



const average = pipe(
  ( xs: number[]) => ( [sum(xs), xs.length] ),
  ( [ total, length ] ) => total / length
)



Enter fullscreen mode Exit fullscreen mode

✅ 我们看到平均值具有正确的类型(xs: number[]) => string并且参数名称被保留。

让我们尝试另一个例子:



const intersparse = pipe( 
  ( text: string, value: string ): [string[], string] => ([ text.split(''), value ]),
  ( [chars, value]: [ string[], string ] ) => chars.join( value )
)


Enter fullscreen mode Exit fullscreen mode

✅ 两个参数名称均被保留(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,
)


Enter fullscreen mode Exit fullscreen mode

✅ 参数名称和类型检查,其类型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>



Enter fullscreen mode Exit fullscreen mode

实现方式仅略有不同



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 ) 
  )
}


Enter fullscreen mode Exit fullscreen mode

让我们来compose测试一下这个新功能:



const longestWordComposeEdition = compose(
  length,
  longestWord,
)


Enter fullscreen mode Exit fullscreen mode

✅ 参数名称和类型检查,其类型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
PREV
使用 Angular、NestJS 和 Nx 构建全栈 Web 应用程序 - 天作之合 为什么要写这篇文章?搭建脚手架 运行项目 调整应用程序以进行开发 从单个服务器提供前端和后端以进行生产 打包应用程序以进行部署
NEXT
作为 ReactJS 开发人员,如何开始使用 React Native?