Typescript recursive function composition

﹥>﹥吖頭↗ 提交于 2019-11-29 12:30:06

Circular type aliases are not really supported except in certain cases. Instead of trying to represent the specific type you've written there in a TypeScript-friendly way, I think I'll back up and interpret your question as: how can we type a flow()-like function, which takes as its arguments a variable number of one-argument functions, where each one-argument-function return type is the argument type for the next one-argument-function, like a chain... and which returns a one-argument function representing the collapsed chain?

I've got something that I believe works, but it's quite complicated, using a lot of conditional types, tuple spreads, and mapped tuples. Here it is:

type Lookup<T, K extends keyof any, Else=never> = K extends keyof T ? T[K] : Else
type Tail<T extends any[]> = 
  ((...t: T) => void) extends ((x: any, ...u: infer U) => void) ? U : 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 LastIndexOf<T extends any[]> =
  ((...x: T) => void) extends ((y: any, ...z: infer U) => void)
  ? U['length'] : never

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

Let's see if it works:

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

Looks good to me.


I'm not sure if it's worth it to go through in painstaking detail about how the type definitions work, but I might as well explain how to use them:

  • Lookup<T, K, Else> tries to return T[K] if it can, otherwise it returns Else. So Lookup<{a: string}, "a", number> is string, and Lookup<{a: string}, "b", number> is number.

  • Tail<T> takes a tuple type T and returns a tuple with the first element removed. So Tail<["a","b","c"]> is ["b","c"].

  • Func1 is just the type of a one-argument function.

  • ArgType<F, Else> returns the argument type of F if it's a one-argument function, and Else otherwise. So ArgType<(x: string)=>number, boolean> is string, and ArgType<123, boolean> is boolean.

  • AsChain<F> takes a tuple of one-argument functions and tries to turn it into a chain, by replacing the return type of each function in F with the argument type of the next function (and using any for the last one). If AsChain<F> is compatible with F, everything's good. If AsChain<F> is incompatible with F, then F is not a good chain. So, AsChain<[(x: string)=>number, (y:number)=>boolean]> is [(x: string)=>number, (y: number)=>any], which is good. But AsChain<[(x: string)=>number, (y: string)=>boolean]> is [(x: string)=>string, (y: string)=>any], which is not good.

  • Finally, LastIndexOf<T> takes a tuple and returns the last index, which we need to represent the return type of flow(). LastIndexOf<["a","b","c"]> is 2.


Okay, hope that helps; good luck!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!