It's too bad that there isn't a convenient way to define this sort of generic variadic compose function. The first time I looked into it I came up with the code in this answer and even though it's been a few years there doesn't seem to be anything dramatically better. For now it seems we just have to muddle through.
In order to get the code from this question to work I needed to make two changes; one minor to suppress an error, and one major to get inference to work. The minor change was to replace
[any, ...Arr][I]
with
Idx<[any, ...Arr], I>
where
type Idx<T, K> = K extends keyof T ? T[K] : never;
Maybe in a perfect world the compiler would understand that if a numeric-like generic index I
is a key of the generic array type Arr
, then it will also be a key of the array type [any, ...Arr]
. We know that's true because of our abilities to reason in the abstract over generic tuples and indices, but the compiler doesn't know this. Someone would have to explicitly program it to check for things like that, which would take extra work to program and then make the compiler do extra work checking for this scenario, and it might not be worth the extra work in either case.
In cases like this where the compiler cannot understand that K
is a key of T
but we need to get T[K]
, we can just use the Idx<T, K>
utility type, which works because there's an explicit check for K extends keyof T
.
The major change was to replace
...fns: { [I in keyof Arr]: I extends 0
? (arg: Begin) => First<Arr> : ⋯ }
with
...fns: [(arg: Begin) => any, ...any] &
{ [I in keyof Arr]: I extends 0
? (arg: Begin) => First<Arr> : ⋯ }
Again, the compiler doesn't have the same ability to reason in the abstract as we do; it unfortunately cannot infer Begin
from a value of type { [I in keyof Arr]: I extends O ? (arg: Begin) => ⋯ : ⋯ }
. There is some support for inferring from homomorphic mapped types (see What does "homomorphic mapped type" mean? for terminology) but that will only allow you to infer the thing being mapped over. So you can possibly infer Arr
from { [I in keyof Arr]: I extends O ? (arg: Begin) => ⋯ : ⋯ }
, but not Begin
.
By intersecting that mapped type with [(arg: Begin) => any, ...any]
, I've added another inference site from which the compiler is able to infer Begin
as "the parameter type of the first argument passed to the function".
Let's try it out:
function a(p: string): boolean { return p === p.toUpperCase() }
function b(p: boolean): number { return p ? 1 : 0 }
function c(p: number): Array<string> {
return Array.from({ length: p }, _ => "" + p)
}
const abc = compose(a, b, c);
// const abc: (arg: string) => string[]
console.log(abc("YES")); // ["1"]
console.log(abc("no")); // []
Looks good. The compiler infers that abc
is of type (arg: string) => string[]
, meaning that Begin
was inferred correctly as string
.
Playground link to code