Seeking a compose
function that enables function composition. The ideal definition of compose
should be type-safe, support any number of arguments, and handle generic or anonymous arguments accurately. However, I've encountered difficulties with the last requirement.
The current definition utilizes Recursive Conditional Types found in Typescript 4.1 Release Candidate:
type Compose<Fns extends any[]> =
Fns extends [(...args: infer Args) => infer Return] ? (...args: Args) => Return :
Fns extends [(...args: infer Args0) => infer Ret0, (arg: infer Arg1) => Ret1, ...infer Rest] ? (
[Ret0, Arg1] extends [Arg1, Ret0] ? Compose<[(...args: Args0) => Ret1, ...Rest]> :
never
) :
never;
declare function compose<Fns extends ((...args: any[]) => any)[]>(
...fns: Fns
): Compose<Fns>;
Works flawlessly when all functions have fixed types:
declare function foo(x1: string, x2: number): number;
declare function bar(y: number): boolean;
declare function baz(z: boolean): string;
const foobarbaz = compose(foo, bar, baz); // (x1: string, x2: number) => string
An issue arises when one function passed to compose
is generic:
declare function foo(x: string): number;
declare function bar<T>(foo: T): string;
const foobar = compose(foo, bar); // typed as `never`
In this case, foobar
becomes never
due to failure in the
[Arg1, Ret0] extends [Ret0, Arg1]
check within Compose
. This occurs because T
, and therefore Arg1
, is inferred as unknown</code. TypeScript might not automatically infer generic parameters here like it does in other cases.</p>
<p>The same issue arises with anonymous functions:</p>
<pre><code>declare function foo(x: string): number;
const foobar = compose(foo, x => x.toLocaleString()); // typed as `(x: string) => any`
In this scenario, x
defaults to any
instead of being implicitly
number</code based on the return value of <code>foo
.
These challenges are anticipated since the restriction on return values and next function's argument stems from Compose
, which is evaluated after compose
's return value. Trying different approaches hasn't been successful yet.
Despite attempts to redefine things for related functions, TypeScript continues to infer any
or
unknown</code, leading to broken typing.</p>
<p>Feel free to reference my attempt:</p>
<pre><code>type Composable<Types extends any[]> = Tail<Types> extends infer Tail ? Tail extends any[] ? {
[I in keyof Tail]: I extends keyof Types ? (arg: Types[I]) => Tail[I] : never;
} : never : never;
declare function compose<Fns extends Composable<T>, T extends any[]>(...fns: Fns): Compose<Fns>;
(Tail
excludes the first member from the array type—here, Tail[I]
refers to the item following Types[I]
)
Unfortunately, using any[]
for T
results in ((arg: any) => any)[]
, offering no solution.
While non-variadic versions of compose
without conditional types may be considered, they are limited to handling predefined numbers of functions. Hence, exploring alternative solutions is crucial to avoid such limitations.