打字稿递归函数组合
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<T, K, Else>
尽可能返回T[K]
,否则返回Else
。所以Lookup<a: string, "a", number>
是string
,Lookup<a: string, "b", number>
是number
。
Tail<T>
采用元组类型T
并返回一个删除了第一个元素的元组。所以Tail<["a","b","c"]>
是["b","c"]
。
Func1
只是单参数函数的类型。
ArgType<F, Else>
如果是单参数函数,则返回 F
的参数类型,否则返回 Else
。所以ArgType<(x: string)=>number, boolean>
是string
,ArgType<123, boolean>
是boolean
。
AsChain<F>
接受一个单参数函数的元组并尝试将其变成一个链,方法是将F
中每个函数的返回类型替换为下一个函数的参数类型(并使用any
表示最后一个)。如果AsChain<F>
与F
兼容,那么一切都很好。如果AsChain<F>
与F
不兼容,那么F
就不是一个好的链。所以,AsChain<[(x: string)=>number, (y:number)=>boolean]>
是[(x: string)=>number, (y: number)=>any]
,这很好。但是AsChain<[(x: string)=>number, (y: string)=>boolean]>
是[(x: string)=>string, (y: string)=>any]
,这样不好。
Last<T>
接受一个元组并返回最后一个元素,我们需要用它来表示flow()
的返回类型。 Last<["a","b","c"]>
是 "c"
。
最后,LaxReturnType<F>
与ReturnType<F>
一样,但对F
没有限制。
好的,希望对您有所帮助;祝你好运!
Playground link to code
【讨论】:
你在这里所做的令人印象深刻。它确实有效。唯一的小挫折是错误似乎总是在第一个函数上,无论链在哪里中断(尽管消息是正确的),这本质上似乎是解决方案的问题。您正在验证 AsChain 中的链,而不是定义类型。主要问题显然是符号的复杂性。为什么不支持递归类型?是否计划在以下版本中支持它们?如果得到支持,我提供的草稿简短、有意识、可维护且易于编写。 “是否计划支持”?在此处阅读“递归类型”设计说明部分:github.com/Microsoft/TypeScript/issues/27102 至于第一个函数总是出错,是的,我注意到了。我不确定递归类型在这方面会更好。这似乎是 IntelliSense 中的一个错误。也许我会报告它。 好吧,我reported the error message issue,这显然是一个错误。也许它会在 TS3.3 中修复? 也许像this?解释可能超出了问题的范围,不适合 cmets。以上是关于打字稿递归函数组合的主要内容,如果未能解决你的问题,请参考以下文章