-
Notifications
You must be signed in to change notification settings - Fork 12.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Spread operator should not have worse ergonomics than apply
- unexpected error spreading a union-of-tuples
#49802
Comments
I've implemented a version of a function call type-checking algorithm that involves spread and there weren't any particular challenges. It's a demo/POC - not a changelist - using a mini-parser with no generics or type inference, but it might be useful if anyone is going to take a look at improving TS support for spread. |
I'd be interested to see the PR. A couple testcases I'd like to see, along with discussion of how they're handled: declare function fn<T>(arg: T, cb: (n: T) => void): void;
declare const t: [number] | [string] | [boolean];
fn(...t, x => {
const n: number | string = x;
}); type Tupleize<T> = T extends unknown ? [T] : never;
type WindowKeys = Tupleize<keyof typeof window>;
declare const c1: WindowKeys;
declare function fn<T>(arg1: T, arg2: T, arg3: T, arg4: T, arg5: T): T;
fn(...c1, ...c1, ...c1, ...c1, ...c1); |
These are interesting test cases, but I don't have a PR, it's a demo and it doesn't involve generics or inference. TS' spread support is currently a bit limited in non-generic, explicitly-typed code which is why I opened this issue with code that doesn't involve generics or inference. It's not obvious to me that inference is the blocker on improvements to spread support, but I assume by your test cases that's a concern for you. In the first code you provide, I see that Ignoring inference for a moment, the 2nd test case is clearly intended to test large unions. In that test case, It's hard to describe optimizations without knowing exactly the current implementation and without measuring particular operations. Given how WindowKeys has been defined, it might be crazy cheap to get back to The way I think about it is this: there's a dead simple algorithm that might be slow in the presence of large unions. Knowing if you have a large union is surely super cheap, so if you don't have the payroll budget for optimization, during compilation you can check the conditions that might blow your runtime budget and produce a compile error. In other words, error only when you see the big unions, and otherwise use the simple algorithm for most cases where there aren't large unions. That's step 1. But, yes, there are optimization possibilities for large unions. Does TS have a defined runtime budget for particular type-checking operations? If you want to point me at the relevant code in your codebase, I'd be interested to take a look, but it's unlikely I'll be producing a PR. |
Non-generic test case that fails in Typescript 4.7.4:
Expected: no error Presumably TS is failing because However, this is too strict, because the following also fails yet the code ensures parameter
If TS was able to handle to the length check, it would be acceptable to fail the case that doesn't check the length. But TS doesn't use the information that the length has been checked, so I believe it would be better to allow both cases instead of preventing both cases. (Note that there is no compile error if |
Another unexpected compile error:
Expected: no error It should compile because the required parameters are guaranteed to be filled. This one is interesting because we can compare it to this case which does compile, so tuple unions occasionally work:
|
This is a fun one because we get a new error message:
Expected: no error
Where does
Perhaps worth comparing to this one which gets us back to the classic error:
Expected: No error The code should compile because |
You can see the spread checking demo at https://callionica.github.io/typescript/type-demo.htm |
Taking a glance through the TS code in Looks like the spread checking code in |
It would be nice for very simple cases to not have this issue, for example: declare function doStuff(name: string, age: number): void
declare function doStuff(employeeId: number): void
declare const args: [string, number] | [number]
// Error described in this issue:
doStuff(...args)
// No error:
if (args.length === 1) {
doStuff(...args)
} else {
doStuff(...args)
} Essentially, if the tuple union can be trivially turned into a single type via a simple condition (such as length) – would it not be possible to iterate over the conditions in the first |
A quick update for v5.0.4 Original repro fails as originally described So no change in behavior with these repros. I'm not sure how I should understand the "Suggestion" label on this bug, but I just want to repeat that I believe that users see this behavior as a defect in TS. |
Not sure what's confusing about my update. I opened this issue to describe buggy behavior in July 2022 and my update is to say that the buggy behavior is still present in TypeScript as of 5.0.4 in April 2023. Hope that helps. |
It'd be great to have better behavior here. One time this comes up is eslint giving It seems to me there should be basic support for this, even if a couple edge cases aren't immediately supported. I agree with @callionica, this feels like a defect. |
This is still an issue in 5.4.2. |
Related on Stack Overflow: spread argument union of tuples |
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 theapply
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) andapply
(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
⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 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 usingapply
: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 theapply
and memberwise versions succeeded.The text was updated successfully, but these errors were encountered: