8

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);
}
3
  • What’s the use case here? Can you show how you intend to call bar2? I’m trying to figure out which workarounds would work for you but I think I’m not seeing the point of F. What narrower types that extend typeof foo are you trying to support? Commented Feb 3, 2021 at 22:41
  • This is a simplified example, but you could imagine scenarios where F is less bounded. In such cases, I could create a wrapper around any function shape F that matches its shape and does some checks before conditionally passing along its arguments to an implementation f: F Commented Feb 3, 2021 at 22:47
  • I think if you are operating over different possible parameter lists and return types, you might want to use those as your generic type parameters instead of the function type... like this. But this depends on your use cases I guess Commented Feb 4, 2021 at 3:09

1 Answer 1

5

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

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

1 Comment

Excellent deep dive, thank you very much! I've marked this as the correct answer as it satisfied my curiosity.Ultimately, I wasn't able to get around this is a satisfactory way without the unsafe option you described, so I fixed my real issue with a bit of a deeper refactor.

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.