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);
}
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 whatF
is, and doesn't want to commit to evaluatingParameters<F>
until it does know it. Which it just won't inside the function, unless you try to assignparams
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 asfoo
does, so it gives an error. It turns out that thebar2()
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):
The fact that this assignment is allowed means that
F extends typeof foo
can be specified with something you're not intending. Imaginefoo()
did something that actually cares about the types of its arguments:Then you could call
bar2()
like this, according to its definition:Since
F
istypeof baz
, thenParameters<F>
is[]
, andbar2()
can be called with no arguments, andparams
might be empty. The error insidebar2
is warning you, correctly, thatfoo(...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 typeF
involved when callingbar2()
, 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-genericbar()
.Finally, if you decide that you don't care about the possibility that
F
will be narrower thanfoo
in such a way as to shorten its argument list, then you can just pretend thatparams
is of typeParameters<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):But be careful!
Playground link to code