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);
}
The error "A spread argument must either have a tuple type or be passed to a rest parameter" occurs when we use the spread syntax with a function that expects a fixed number of parameters. To solve the error, use a tuple instead of an array or type the function to use a rest parameter.
To type a function as a parameter, type the function's parameter list and its return value, e.g. doMath: (a: number, b: number) => number . If the function's definition becomes too busy, extract the function type into a type alias.
This article opts to use the term type variables, coinciding with the official Typescript documentation. T stands for Type, and is commonly used as the first type variable name when defining generics. But in reality T can be replaced with any valid name.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With