TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Spread operator should not have worse ergonomics than `apply` - unexpected error spreading a union-of-tuples

Open callionica opened this issue 2 years ago • 14 comments

Bug Report

Attempting to spread a union-of-tuples that is otherwise compatible with the function being called results in an unexpected compiler error:

4.3.5+ "A spread argument must either have a tuple type or be passed to a rest parameter." 3.33-4.2.3 "Expected 2 arguments, but got 0 or more."

However, calling apply on the function with the same union-of-tuples does not produce an error, and neither does calling the function "memberwise" using indexes into the union-of-tuples. In both the apply case and the memberwise case, the user is getting the benefit of some level of typechecking, whereas when using the spread operator, the user is met with a hard error.

The error message is confusing, but more confusing is why there would even be an error. I am opening this issue primarily to address the error, not the message.

Since there is a very clear relationship between apply, a memberwise call, and the spread operator, it is perplexing why spread does not work in this case. I imagine that hitting this kind of error might make JS switchers to TS quite confused and possibly cause them to abandon ship. That same relationship between spread (which does not work as expected) and apply (which works adequately) also points to at least one possible path to implementing the desired behavior of successfully typechecking a spread involving a union-of-tuples.

🔎 Search Terms

spread, apply, union of tuples, A spread argument must either have a tuple type or be passed to a rest parameter

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about spread

⏯ Playground Link

Playground link with relevant code

💻 Code

// A function that takes two arguments:
function fn(a: string | number, b: string | number) {
    console.log(a, b);
}

// A union-of-2-tuples type
type T = [string, string] | [number, number];

// A union-of-tuples variable
let t : T = Math.random() > 0.5 ? ["A", "B"] : [1, 1];

// Can we call the fn member-wise with a union-of-tuples? Yes.
fn(t[0], t[1]);

// Can we call the function through apply with a union-of-tuples? Yes.
fn.apply(null, t);

// Note that in both cases above type checking is happening! 

// Can we spread the variable? No.
fn(...t); // ERROR: A spread argument must either have a tuple type or be passed to a rest parameter.

🙁 Actual behavior

In the code above, you can see that we have a variable t that is a union-of-tuples.

Each tuple in the union is type-compatible with the function fn.

You can see that we are able to call the function using the members of t without any casts nor compiler errors: fn(t[0], t[1]).

You can also see that we are able to pass t to the function in one go using apply: fn.apply(null, t). Again, this is successful and has no casts or other code artifacts.

Finally, we attempt to call the function by spreading t: fn(...t). This alone causes the compiler to produce an error.

🙂 Expected behavior

I would expect the code using the spread operator, fn(...t), to succeed, just as the apply and memberwise versions succeeded.

callionica avatar Jul 06 '22 10:07 callionica