打字稿递归函数组合

Posted

技术标签:

【中文标题】打字稿递归函数组合【英文标题】:Typescript recursive function composition 【发布时间】:2019-04-09 22:09:03 【问题描述】:

我想创建一个函数链,它是管道/流/组合函数的输入。

如果没有将类型的字面扩展至选定深度,这是否可能,就像通常处理的那样? See lodash's flow.

我想实现链中数据流的类型检查。 - 函数的参数是前一个的结果 - 第一个参数是模板参数 - 最后返回的是一个模板参数

type Chain<In, Out, Tmp1 = any, Tmp2 = any> = [] | [(arg: In) => Out] | [(arg: In) => Tmp1, (i: Tmp1) => Tmp2, ...Chain<Tmp2, Out>];

这个想法在草稿中。

然而,这会产生以下错误:

    Type alias 'Chain' circularly references itself.(明白为什么,不知道怎么解决) A rest element type must be an array type.(可能 spread 不适用于泛型元组) Type 'Chain' is not generic.(甚至不明白为什么会出现这个错误)

Chain 在 Typescript 中的定义是否可行?如果是这样,请附上一个sn-p。

(在最新的 tsc 3.1.6 上测试)

【问题讨论】:

这不是“一路乌龟”的问题吗?即不可能解析递归类型。 【参考方案1】:

除非在某些情况下,否则并不真正支持循环类型别名。 (更新 TS 4.1,这些现在得到更多支持,但我仍然倾向于将 flow() 表示为在 AsChain 上运行,验证特定的函数数组,而不是试图提出一个Chain 匹配所有有效函数数组)

与其试图以 TypeScript 友好的方式表示您在那里编写的特定类型,我想我会备份并将您的问题解释为:我们如何键入类似 flow() 的函数,它需要作为其参数的可变数量的单参数函数,其中每个单参数函数的返回类型是下一个单参数函数的参数类型,就像一个链......并且它返回一个单参数函数,表示倒塌的链条?

我有一些我认为可行的方法,但它非常复杂,使用了很多 conditional types、tuple spreads 和 mapped tuples。这里是:

type Lookup<T, K extends keyof any, Else=never> = K extends keyof T ? T[K] : Else

type Tail<T extends any[]> = T extends [any, ...infer R] ? R : never;

type Func1 = (arg: any) => any;
type ArgType<F, Else=never> = F extends (arg: infer A) => any ? A : Else;
type AsChain<F extends [Func1, ...Func1[]], G extends Func1[]= Tail<F>> =
   [K in keyof F]: (arg: ArgType<F[K]>) => ArgType<Lookup<G, K, any>, any> ;

type Last<T extends any[]> = T extends [...infer F, infer L] ? L : never;
type LaxReturnType<F> = F extends (...args: any) => infer R ? R : never;

declare function flow<F extends [(arg: any) => any, ...Array<(arg: any) => any>]>(
  ...f: F & AsChain<F>
): (arg: ArgType<F[0]>) => LaxReturnType<Last<F>>;

让我们看看它是否有效:

const stringToString = flow(
  (x: string) => x.length, 
  (y: number) => y + "!"
); // okay
const str = stringToString("hey"); // it's a string

const tooFewParams = flow(); // error

const badChain = flow(
  (x: number)=>"string", 
  (y: string)=>false, 
  (z: number)=>"oops"
); // error, boolean not assignable to number

我觉得不错。


我不确定是否值得详细了解类型定义的工作原理,但我不妨解释一下如何使用它们:

Lookup&lt;T, K, Else&gt; 尽可能返回T[K],否则返回Else。所以Lookup&lt;a: string, "a", number&gt;stringLookup&lt;a: string, "b", number&gt;number

Tail&lt;T&gt; 采用元组类型T 并返回一个删除了第一个元素的元组。所以Tail&lt;["a","b","c"]&gt;["b","c"]

Func1 只是单参数函数的类型。

ArgType&lt;F, Else&gt; 如果是单参数函数,则返回 F 的参数类型,否则返回 Else。所以ArgType&lt;(x: string)=&gt;number, boolean&gt;stringArgType&lt;123, boolean&gt;boolean

AsChain&lt;F&gt; 接受一个单参数函数的元组并尝试将其变成一个链,方法是将F 中每个函数的返回类型替换为下一个函数的参数类型(并使用any 表示最后一个)。如果AsChain&lt;F&gt;F 兼容,那么一切都很好。如果AsChain&lt;F&gt;F不兼容,那么F就不是一个好的链。所以,AsChain&lt;[(x: string)=&gt;number, (y:number)=&gt;boolean]&gt;[(x: string)=&gt;number, (y: number)=&gt;any],这很好。但是AsChain&lt;[(x: string)=&gt;number, (y: string)=&gt;boolean]&gt;[(x: string)=&gt;string, (y: string)=&gt;any],这样不好。

Last&lt;T&gt; 接受一个元组并返回最后一个元素,我们需要用它来表示flow() 的返回类型。 Last&lt;["a","b","c"]&gt;"c"

最后,LaxReturnType&lt;F&gt;ReturnType&lt;F&gt; 一样,但对F 没有限制。


好的,希望对您有所帮助;祝你好运!

Playground link to code

【讨论】:

你在这里所做的令人印象深刻。它确实有效。唯一的小挫折是错误似乎总是在第一个函数上,无论链在哪里中断(尽管消息是正确的),这本质上似乎是解决方案的问题。您正在验证 AsChain 中的链,而不是定义类型。主要问题显然是符号的复杂性。为什么不支持递归类型?是否计划在以下版本中支持它们?如果得到支持,我提供的草稿简短、有意识、可维护且易于编写。 “是否计划支持”?在此处阅读“递归类型”设计说明部分:github.com/Microsoft/TypeScript/issues/27102 至于第一个函数总是出错,是的,我注意到了。我不确定递归类型在这方面会更好。这似乎是 IntelliSense 中的一个错误。也许我会报告它。 好吧,我reported the error message issue,这显然是一个错误。也许它会在 TS3.3 中修复? 也许像this?解释可能超出了问题的范围,不适合 cmets。

以上是关于打字稿递归函数组合的主要内容,如果未能解决你的问题,请参考以下文章

递归打字稿类型和反应形式

打字稿:如何制作接受对象的类型,其键匹配通用但所有值都是值参数的映射函数

有没有更好的方法在打字稿中编写这种递归方法

打字稿数组类型,其中输出是动态输入数量的所有可能组合

将递归函数重写为管道函数组合

递归函数二分查找面相对象初识类空间,对象空间组合继承