2

I want to write a call function:

function call<F extends (...arg: any) => any, P extends Parameters<F>>(
  fn?: F,
  ...arg: P
): ReturnType<F> | undefined {
  if (fn) return fn(...arg) // Type 'P' must have a '[Symbol.iterator]()' method that returns an iterator
}

const fn = (a: string) => {}

// automatically infer `fn` arguments type
call(fn, )

what should I do?

3 Answers 3

1

This is a known bug in TypeScript; see microsoft/TypeScript#36874. It looks like the problem is triggered by F extends (...arg: any) => any instead of F extends (...arg: any[]) => any. That is, your arg rest parameter is of the anything-at-all any type instead of the array-of-anything any[] type. The obvious workaround is therefore to change any to any[], which shouldn't be a problem because function rest parameters are essentially always arraylike:

function call<F extends (...arg: any[]) => any, P extends Parameters<F>>(
  fn?: F, ...arg: P): ReturnType<F> | undefined {
  return fn?.(...arg) // okay (I used optional chaining here to deal with missing fn)
}

Looks good!


That's the answer to the question as asked, although it is useful to note that generally speaking, the preferred way to do this is to make call() generic in P (the rest parameter type) and R (the return type), and not use the Parameters<T> and the ReturnType<T> utility types:

function call<P extends any[], R>(fn?: (...arg: P) => R, ...arg: P): R | undefined {
  return fn?.(...arg); // okay
}

They are similar, but your approach ends up being unable to properly represent what happens when fn is itself generic:

const pair = <T, U>(left: T, right: U): [T, U] => [left, right];

const ret = call(pair, "abc", 123); // old version of call
// const ret: [any, any] | undefined ☹

whereas the approach I recommend can take advantage of the support for higher order type inference from generic functions:

const ret = call(pair, "abc", 123); // new version of call
// const ret: [string, number] | undefined 👍

If you can rewrite generics using conditional types like Parameters<T> and ReturnType<T>, it will sometimes improve the compiler's ability to produce desirable results.

Playground link to code

Sign up to request clarification or add additional context in comments.

1 Comment

👍👍 Your answer is detailed, no doubt I will use the way you recommended, thank you very much.
0

This is a tricky one, but the solution is not.

You have to ensure the typescript that args is iterable. You can do that like manually saying to him - Hey this will be an array, believe me

Working playground

function call<F extends (...arg: any) => any, P extends Parameters<F>>(
  fn?: F,
  ...args: P
): ReturnType<F> | undefined {
  if (fn){
      // here is the ugly hint to typescript
      return fn(...(args as any[]))
  } 
  return undefined;
}

2 Comments

Means I have to use type assertion?but shouldn't the type P be an array?
typescript is not sure P is "spreadable"/ "iterable" evn though for humans it obviously is.
0

Have you considered using apply as a workaround?

function call<F extends (...arg: any) => any, P extends Parameters<F>>(
  fn?: F,
  ...arg: P,
): ReturnType<F> | undefined {
  if (fn) return fn.apply(undefined, arg);

  return undefined;
}

Playground

1 Comment

Thanks, this is a new solution that I did not expect, I will try it tomorrow

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.