Skip to content
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

Proposal: Support spread operator for arrays and tuples in function calls #5296

Closed
sandersn opened this issue Oct 16, 2015 · 9 comments
Closed
Assignees
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@sandersn
Copy link
Member

Currently, Typescript does not support all uses of Ecmascript's spread operator. Specifically, in function calls, Typescript only supports spreading array arguments that match rest parameters. #4130 is a recent mention of this limitation. I propose two ways of addressing this limitation in function calls:

  1. Support array arguments with the spread operator that do not match rest parameters.
  2. Support tuple arguments with the spread operator.

Although (2) is cleaner from a type checker's point of view, I believe that (1) is more useful because it can type more of the uses that Javascript programmers are accustomed to writing. Of course, both proposals could be implemented.

Proposal 1: Spread operator supports array arguments

The spread operator may be used with the last array argument of a function call, even when that argument doesn't match a rest parameter in the function definition. However, the parameters must all be optional and of the same type. The type must match the element type of the array.
For example,

function f(s: string, a?: number, b?: number,  ...rest: number[]) {
    // ...
}
function g(s: string, a?: number, b?: number) {
    // ...
}
// legal
let long: number[] = [1,2,3];
let medium: number[] = [1,2];
let short: number[] = [1];
let empty: number[] = [];
f("foo", ...long); // and medium, short and empty
f("foo", ...long, 1, ...long); // and medium, short and empty
g("foo", ...long); // and medium, short and empty
// illegal
f(...["foo", 1, 2]); // array of type (number | string)[] doesn't match any parameter type
g("foo", ...long, 1); // 1 doesn't match any parameter
g("foo", ...empty, 1); // 1 would match, but the compiler can't tell

The checker consumes all parameters that match the array type after the spread operator is used, until

  1. A non-matching type is encountered, causing an error.
  2. The end of the parameter list is encountered, succeeding.
  3. A rest parameter is encountered, succeeding if the rest parameter's array type matches the spread argument's array type.
  4. TODO: These rules don't account for trailing arguments that also match a rest parameter.

At runtime the spread operator has EcmaScript semantics. Basically, undefined if the array is too short, and extra arguments are ignored.

Proposal 2: Spread operator supports tuple arguments

The spread operator may be used with a tuple argument at any position in a function call. Extra members of the tuple will even match rest parameters if the tuple is long enough.

function f(s: string, a: number, b: number, c: number,  ...rest: number[]) {
    // ...
}
function g(s: string, a: number, b: number) {
}
// legal
let quad: [string, number, number, number, number] = ["foo", 1,2,3,4];
let triple: [string, number, number, number] = ["foo", 1,2,3];
let double: [string, number, number] = ["foo", 1,2];
f(...quad); // and triple
f(...double, 3);
f("foo", ...[1,2,3]);
f(...quad, 1, ...[1,2,3]); // and triple
g(...double);
// illegal
f(...double); // too short -- double doesn't match parameter 'c'
g(...triple); // too many parameters
g(...quad); // too many parameters

The checker matches each member of the spread tuple argument with one parameter until

  1. A member's type does not match a parameter type, causing an error.
  2. The end of the parameter list is encountered, succeeding if there are no more tuple members.
  3. A rest parameter is encountered, succeeding if the remaining tuple members are typed as an array and that type matches the rest parameter's type.
  4. TODO: These rules don't account for trailing arguments that also match a rest parameter.

Notes

The tuple proposal has the problem that it types tuples as tuples in function applications only -- as arrays elsewhere. This is inconsistent, even if function application is arguably the most tuple-like of locations. And it's not clear whether either proposal will allow real uses of the spread operator that Javascript programmers are expecting.

If both proposals are implemented, function calls with spread array literals work in more cases if the literal is always typed as tuple, and provide more and better errors in the cases that do not type check:

f(...["bar",1,2,3,4,5]) // tuple gives no error; array gives error: bad type (number | string)[]
g(...["bar",1,2,3,4,5]) // tuple gives error: too long; array gives error bad type (number | string)[]
function rest(a?: number, b?: number, ...rest:number[]);
function fixed(a: number, b: number);
rest(...[1,2,3,4,5]); // tuple gives no error; array gives no error
fixed(...[1,2,3,4,5]); // tuple gives error: too long; array gives no error

So spread array literals should always be interpreted as tuples.

@sandersn sandersn added the Suggestion An idea for TypeScript label Oct 16, 2015
@DanielRosenwasser
Copy link
Member

Seems like this would also solve #4759.

@sandersn
Copy link
Member Author

@ahejlsberg, you mentioned that you were interested in this proposal.

@niieani
Copy link

niieani commented Jun 28, 2016

This is a pretty basic and useful feature (e.g. for restoring JSON defaults).
Is there any workaround to make this work?
Typing the input array as any doesn't have any effect: :(

function test(a: string, b: {}, c: number) {}

test('one', {}, 3); // OK

let params = ['one', {}, 3];
test(...params); // ERROR - as expected

test(...(params as any)); // ERROR - no luck here either :-(

@DanielRosenwasser
Copy link
Member

You can assert the type of the function to any:

(test as any)(...params);

@niieani
Copy link

niieani commented Jun 28, 2016

Ah, right. Thanks @DanielRosenwasser. Hope this gets fixed soon!

@Igorbek
Copy link
Contributor

Igorbek commented Sep 30, 2016

I really like the idea of tuples being matched formal arguments. However currently it is problematic because of the way tuples types. Consider your example:

function f(s: string, a: number, b: number, c: number,  ...rest: number[]) {
    // ...
}
// legal
let quad: [string, number, number, number, number] = ["foo", 1,2,3,4];
f(...quad); // and triple

quad = ["foo", 1, 2, 3, 4, "boo!"]; // it is legal currently
f(...quad); // now it blows up in run time because rest has a string

So that issue could be addressed by making tuples more restrictive in assignability (my proposal #6229 sorry)

Other thought regarding spreads and tuples:

  • it'd be nice to type [...[1, "a"]] as [number, string] (or [widening 1, widening "a"] now?). Of course there could be any tuple passed. It looks similar to object spread operator, and to properly support generic cases, similarly to Add spread/rest higher-order types operator #10727 would be nice to have a type operator [T, ...U] where T and U could be tuples, arrays, or iterators. @sandersn if you don't mind I'm gonna make a proposal of such type operator by following your example of object spread
  • I already mentioned in Variadic Kinds Proposal Proposal: Variadic Kinds -- Give specific types to variadic functions #5453 that tuples seemed to be a perfect match for capturing variadic generic types. If we treated f(1, "a") to be equivalent to f(...[1, "a"]) then a single tuple type could parameterize any sequence of formal parameters. It would mean that variadic kinds were not really a special case.
  • I'm more and more convinced that tuples are really need this Proposal: strict and open-length tuple types #6229. It's really a blocker of further adoption of that nice duality between spreaded tuples and abstract list of ordered typed data.

@josundt
Copy link

josundt commented Jan 27, 2017

I was surprised when finding that tuple type variables cannot be used with the spread operator in function calls. When the function parameter's types match the tuple's item types I thought this should work.

I was convinced that this was working in TypeScript by now (2.1), and struggled to figure out what I was doing wrong.

I don't immediately see any progress indication on this issue.
Will this be supported any time soon?

@sandersn
Copy link
Member Author

No, the typescript team proper hasn't touched this issue in a while. @Igorbek has contributed a lot to the discussion in the meantime — his summary in the post above is a useful summary of the current state. Most discussion of the problem has occurred at #5453 and #6229.

@mhegazy
Copy link
Contributor

mhegazy commented May 10, 2017

Based on the discussion in #15747, we would like to pursue this change for arrays, but no special treatment for tuples at the time being.

@mhegazy mhegazy added Committed The team has roadmapped this issue and removed In Discussion Not yet reached consensus labels May 10, 2017
@mhegazy mhegazy added this to the TypeScript 2.4 milestone May 10, 2017
@sandersn sandersn added Fixed A PR has been merged for this issue and removed Committed The team has roadmapped this issue labels May 16, 2017
calvellido added a commit to frees-io/freestyle-opscenter-webclient that referenced this issue Sep 29, 2017
* This is due to a bug about spread operator usage fixed on TypeScript 2.4.0 (microsoft/TypeScript#5296)
* With this change now a warning will be given about the TS version used. This is expected, you see the discussion about it  on angular/angular-cli#7625
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants