1

I tried to add type of a function named 'compose', as follow:

type First<T> = T extends [infer U, ...any[]] ? U : never

type Last<T> = T extends [...any[], infer U] ? U : never

// compose :: (f, g, ...) -> (a -> f(g(...(a))) )
function compose<Arr extends any[], Begin extends any>(
  ...fns: {
    [I in keyof Arr]: I extends 0
      ? (arg: Begin) => First<Arr>
      : (arg: [any, ...Arr][I] /**wrong: Validation type 'I' cannot be used for index type '[any,... Arr]' */) => Arr[I]
  }
): (arg: Begin) => Last<Arr> {
  return function (val) {
    return fns.reduce((input, fn) => {
      return fn(input)
    }, val) as any
  }
}

as above, typescript shows: Validation type 'I' cannot be used for index type '[any,... Arr]',

when I use some examples to test this function, another type problem happened:

function a(p: string): boolean {
  return true
}

function b(p: boolean): number {
  return 1
}

function c(p: number): Array<string> {
  return []
}

const abc = compose(a, b, c)

the abc function's param is unknown, I can't solve these two problems, so I ask for help, playground: playground

solve two problems above

jcalz
  • 264,269
  • 27
  • 359
  • 360
jimin hu
  • 55
  • 5
  • Does [this approach](https://tsplay.dev/wj0Q1m) meet your needs? If so I could write up an answer; if not, what am I missing? – jcalz Apr 03 '23 at 14:58
  • Thank you! the result is right! but what confuses me is that I obviously use Begin to represent the parameter of the first function,but the parameter of 'cops' function is uknown, what's wrong with this? Expect your awesome answer – jimin hu Apr 04 '23 at 00:40
  • When you say "obviously", you mean "for a human being", but TS's inference algorithm doesn't have human-level intelligence. So it can't infer `Begin` from a value of type `{ [I in keyof Arr]: I extends 0 ? (arg: Begin) => any; any }`; inferring from a homomorphic mapped type like that will let you infer the thing you're mapping over (`Arr`) but not other stuff that appears in there. On the other hand, it *can* infer `Begin` from a value of type `[(arg: Begin)=>any, ...any]`. So I had to intersect those to give it an inference site. I'll write up an answer when I get a chance. – jcalz Apr 04 '23 at 00:45
  • Also, could you [edit] to fix your `Fist` typo to be `First` (I assume), and is there any meaning to the name `cops`? That word generally means "police officers" to me, and it's distracting. If it doesn't mean anything, I'd rather see `foo` or `abc` or something there. Otherwise, can you explain it? – jcalz Apr 04 '23 at 00:47
  • It's my bad, truly I means "First", and also the 'cops' is quite a bad name for a function. – jimin hu Apr 04 '23 at 01:05

1 Answers1

1

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

jcalz
  • 264,269
  • 27
  • 359
  • 360