Typescript: pass Parameters<T> tuple of generic function type parameter as variadic arguments

10.6k Views Asked by At

In Typescript 4.1.x you can spread tuple types into functions as variadic arguments.

type MyTuple = [string, number];
const myTuple: MyTuple = ["blob", 42];

function tupleFunc(s: string, n: number): void {
    console.log(`${s} ${n}`);
}

tupleFunc(...myTuple); // ✅ A-Ok

However, I'm hitting an error when the tuple is derived from a generic type parameter and using the Parameters<T> utility type.

function foo(a: number, b: boolean, c: string) {
  return 10;
}

foo(1, true, "baz") // 10 ✅

function bar(...params: Parameters<typeof foo>) {
    return foo(...params)
}

bar(1, true, "baz") // 10 ✅

function bar2<F extends typeof foo>(...params: Parameters<F>) {
  //  next line would work
  //  return foo(params[0], params[1], params[2])

  return foo(...params); // Fails 
  // Expected 3 arguments, but got 0 or more.ts(2556)
  // index.ts(28, 14): An argument for 'a' was not provided.
}

Is there a way to make this concept pass the type checker or is it not supported in typescript? Although it errors, it seems to work in the Typescript sandbox. See an example here.

Seems like I can get it to work with .apply(), but I'd love to know if there's another way.

function bar3<T extends typeof foo>(...params: Parameters<T>) {
  return foo.apply(null, params);
}
1

There are 1 best solutions below

1
On BEST ANSWER

This is interesting and I don't know if there's a canonical GitHub issue about it. Haven't found one yet; I'll come back and edit if I find one. My best guess about the cause for the error is that the Parameters<F> utility type inside of the function implementation is an "unresolved generic conditional type"; the compiler doesn't know what F is, and doesn't want to commit to evaluating Parameters<F> until it does know it. Which it just won't inside the function, unless you try to assign params to another variable or use a type assertion.


The compiler apparently does not know for sure that F, whatever it is, will have as many arguments as foo does, so it gives an error. It turns out that the bar2() implementation is unsafe.

One of the assignability rules in TypeScript is that a function of fewer parameters is assignable to a function of more parameters. See the FAQ entry on the subject for why this is desirable (short answer: it's usually safe to assume a function will just ignore extra arguments, and this is how most people write callbacks that don't need all the passed-in parameters):

const baz = () => "hello";
const assignableToFoo: typeof foo = baz; // no error

The fact that this assignment is allowed means that F extends typeof foo can be specified with something you're not intending. Imagine foo() did something that actually cares about the types of its arguments:

function foo(a: number, b: boolean, c: string): string {
  return `success ${a.toFixed(2)} ${b} ${c.toUpperCase()}`;
}

Then you could call bar2() like this, according to its definition:

console.log(bar2<typeof baz>()); // compiles fine, but:
// RUNTIME ERROR  TypeError: a.toFixed() no good if a is undefined!

Since F is typeof baz, then Parameters<F> is [], and bar2() can be called with no arguments, and params might be empty. The error inside bar2 is warning you, correctly, that foo(...params) is potentially dangerous.


Now because you said that this is a simplified example, I'm not 100% sure how best to write a version of bar2's signature that captures the desired use cases. Usually generic type parameters should correspond to some actual value; but there is no value of type F involved when calling bar2(), just a value whose type is the same as its argument list. With the example code as written, I'd say that you should just use your non-generic bar().


Finally, if you decide that you don't care about the possibility that F will be narrower than foo in such a way as to shorten its argument list, then you can just pretend that params is of type Parameters<typeof foo> (in fact I think the compiler will let you unsafely "widen" it to a variable of that type, even though it probably shouldn't):

function bar2<F extends typeof foo>(...params: Parameters<F>) {
  const _params: Parameters<typeof foo> = params;
  return foo(..._params); // no error now
}

But be careful!


Playground link to code