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
bar2? I’m trying to figure out which workarounds would work for you but I think I’m not seeing the point ofF. What narrower types that extendtypeof fooare you trying to support?Fis less bounded. In such cases, I could create a wrapper around any function shapeFthat matches its shape and does some checks before conditionally passing along its arguments to an implementationf: F