Circular type aliases are not widely supported, except in specific scenarios. (UPDATE: As of TS 4.1, there is better support for circular type aliases. However, representing flow()
as operating on AsChain
that verifies a specific array of functions seems more viable than trying to match all valid arrays of functions with a single Chain
)
Instead of directly translating the particular type you've defined into TypeScript syntax, I'll consider your question as follows: how can we type a function like flow()
, which accepts multiple one-argument functions and connects them into a chain where each function's return type matches the next function's argument type... ultimately returning a collapsed chain as a single one-argument function?
I have come up with a solution that seems to work, although it involves some complexity with the use of conditional types, tuple spreads, and mapped tuples. Here is the implementation:
// Type Definitions
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;
// Function Declaration
declare function flow<F extends [(arg: any) => any, ...Array<(arg: any) => any>]>(
...f: F & AsChain<F>
): (arg: ArgType<F[0]>) => LaxReturnType<Last<F>>;
Testing the Implementation:
// Test Cases
const stringToString = flow(
(x: string) => x.length,
(y: number) => y + "!"
); // okay
const str = stringToString("hey"); // it produces a string
const tooFewParams = flow(); // error
const badChain = flow(
(x: number)=>"string",
(y: string)=>false,
(z: number)=>"oops"
); // error, boolean not assignable to number
The explanation of type definitions is intricate, but here's a brief guide on using them:
Lookup<T, K, Else>
tries to retrieve T[K]
, falling back to Else
if it doesn't exist. For instance,
Lookup<{a: string}, "a", number>
results in string
, and Lookup<{a: string}, "b", number>
yields number
.
Tail<T>
removes the first element from a tuple type T
. Thus,
Tail<["a","b","c"]>
becomes ["b","c"]
.
Func1
represents a one-argument function.
ArgType<F, Else>
retrieves the argument type of F
if it's a one-argument function; otherwise, it returns Else
. For example,
ArgType<(x: string)=>number, boolean>
is string
, while ArgType<123, boolean>
is boolean
.
AsChain<F>
transforms a tuple of one-argument functions into a chain by adjusting each function's return type to match the subsequent function's argument type (using any
for the last function). If AsChain<F>
aligns with F
, everything works correctly. Otherwise, F
is an invalid chain. So,
AsChain<[(x: string)=>number, (y:number)=>boolean]>
would be [(x: string)=>number, (y: number)=>any]
, which is acceptable. But AsChain<[(x: string)=>number, (y: string)=>boolean]>
is [(x: string)=>string, (y: string)=>any]
, deemed incorrect.
Last<T>
picks out the last element from a tuple, essential for representing the return type of flow()
. For instance,
Last<["a","b","c"]>
gives "c"
.
LaxReturnType<F>
resembles ReturnType<F>
without constraints on F
.
That sums up the overview. Best of luck with your coding endeavors!
Link to Playground code