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

Tuples in rest parameters and spread expressions #24897

Merged
merged 40 commits into from
Jun 26, 2018
Merged

Tuples in rest parameters and spread expressions #24897

merged 40 commits into from
Jun 26, 2018

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Jun 12, 2018

This PR includes the following:

  • Expansion of rest parameters with tuple types into discrete parameters.
  • Expansion of spread expressions with tuple types into discrete arguments.
  • Generic rest parameters and corresponding inference of tuple types.
  • Optional elements in tuple types.
  • Rest elements in tuple types.

With these features it becomes possible to strongly type a number of higher-order functions that transform functions and their parameter lists (such as JavaScript's bind, call, and apply).

The PR implements the a number of the capabilities discussed in #5453, but with considerably less complexity.

Rest parameters with tuple types

When a rest parameter has a tuple type, the tuple type is expanded into a sequence of discrete parameters. For example the following two declarations are equivalent:

declare function foo(...args: [number, string, boolean]): void;
declare function foo(args_0: number, args_1: string, args_2: boolean): void;

EDIT: With #26676 the type of rest parameter can be a union of tuple types. This effectively provides a form of overloading expressed in a single function signature.

Spread expressions with tuple types

When a function call includes a spread expression of a tuple type as the last argument, the spread expression corresponds to a sequence of discrete arguments of the tuple element types. Thus, the following calls are equivalent:

const args: [number, string, boolean] = [42, "hello", true];
foo(42, "hello", true);
foo(args[0], args[1], args[2]);
foo(...args);

Generic rest parameters

A rest parameter is permitted to have a generic type that is constrained to an array type, and type inference can infer tuple types for such generic rest parameters. This enables higher-order capturing and spreading of partial parameter lists:

declare function bind<T, U extends any[], V>(f: (x: T, ...args: U) => V, x: T): (...args: U) => V;

declare function f3(x: number, y: string, z: boolean): void;

const f2 = bind(f3, 42);  // (y: string, z: boolean) => void
const f1 = bind(f2, "hello");  // (z: boolean) => void
const f0 = bind(f1, true);  // () => void

f3(42, "hello", true);
f2("hello", true);
f1(true);
f0();

In the declaration of f2 above, type inference infers types number, [string, boolean] and void for T, U and V respectively.

Note that when a tuple type is inferred from a sequence of parameters and later expanded into a parameter list, as is the case for U, the original parameter names are used in the expansion (however, the names have no semantic meaning and are not otherwise observable).

Optional elements in tuple types

Tuple types now permit a ? postfix on element types to indicate that the element is optional:

let t: [number, string?, boolean?];
t = [42, "hello", true];
t = [42, "hello"];
t = [42];

In --strictNullChecks mode, a ? modifier automatically includes undefined in the element type, similar to optional parameters.

A tuple type permits an element to be omitted if it has a postfix ? modifier on its type and all elements to the right of it also have ? modifiers.

When tuple types are inferred for rest parameters, optional parameters in the source become optional tuple elements in the inferred type.

The length property of a tuple type with optional elements is a union of numeric literal types representing the possible lengths. For example, the type of the length property in the tuple type [number, string?, boolean?] is 1 | 2 | 3.

Rest elements in tuple types

EDIT: Updated to reflect change from string* to ...string[] syntax for rest elements.

The last element of a tuple type can be a rest element of the form ...X, where X is an array type. A rest element indicates that the tuple type is open-ended and may have zero or more additional elements of the array element type. For example, [number, ...string[]] means tuples with a number element followed by any number of string elements.

function tuple<T extends any[]>(...args: T): T {
    return args;
}

const numbers: number[] = getArrayOfNumbers();
const t1 = tuple("foo", 1, true);  // [string, number, boolean]
const t2 = tuple("bar", ...numbers);  // [string, ...number[]]

The type of the length property of a tuple type with a rest element is number.

Strong typing of bind, call, and apply

With this PR it becomes possible to strongly type the bind, call, and apply methods on function objects. This is however a breaking change for some existing code so we need to investigate the repercussions.

Fixes #1024.
Fixes #4130.
Fixes #5331.

@Kingwl
Copy link
Contributor

Kingwl commented Jun 12, 2018

thanks for your great working🤩

@felixfbecker
Copy link
Contributor

felixfbecker commented Jun 12, 2018

This looks incredible and I can't wait to make use of this.


Open-ended tuple types (in progress).

That sounds amazing - this would allow us to type the return value of string.split() as always having at least one element:

split(delimiter: string): [string, string*]

In a world where we have #13778, that would mean the types for destructured split would be correctly inferred:

// [string, string | undefined]
const [package, version] = '[email protected]'.split('@')

The * Kleene star looks a bit weird because it is not used for rest syntax in JS. Maybe spread syntax could be used? This would align with a future generic type spread operator: [string, ...number[]]
The inner array type is spread into the tuple.

@jovdb
Copy link

jovdb commented Jun 12, 2018

Great!,
I think you made a typo in the comment of f1. shouldn't z be of type boolean?

const f1 = bind(f2, "hello");  // (z: boolean) => void

@Cryrivers
Copy link

Great work! Now TypeScript is one step closer to #5453!

Rather than having [string, number*], could we support spread operator in types as well, so we could have something like [string, ...number[]]?

@Strate
Copy link

Strate commented Jun 12, 2018

@Cryrivers

Rather than having [string, number*], could we support spread operator in types as well, so we could have something like [string, ...number[]]?

Or just have multiple rest arguments: (a: string, ...rest: [string, number], ...restLast: string[])

@goodmind
Copy link

But tuples can't be infinite by definition?

# Conflicts:
#	tests/baselines/reference/api/tsserverlibrary.d.ts
#	tests/baselines/reference/api/typescript.d.ts
@j-oliveras
Copy link
Contributor

@goodmind before this PR, tuples are fixed sized.

@ahejlsberg
Copy link
Member Author

@jovdb Yes, that was a typo. Now fixed. Thanks for catching.

@goodmind
Copy link

@ahejlsberg can this be used to type compose, pipe or curry?

@ahejlsberg
Copy link
Member Author

@felixfbecker @Cryrivers There are two ways we could go on the syntax for the last element in an open-ended tuple:

[string, number*]
[string, ...number[]]

The latter is longer but certainly makes it easier to see the connection to rest parameters. If we go with the latter, the last element will be required to have an array type. This PR will specifically not permit the equivalent of generic rest parameters in tuples--that would add considerable complexity. But certainly the ...X syntax would better accommodate that feature later on, should we choose to go there.

@felixfbecker
Copy link
Contributor

I think the last type being required to be an array is perfectly acceptable.

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Jun 12, 2018

This is a wonderful step forward!

+1 to the [string, ...number[]] syntax, I feel despite its verbosity it would keep the syntax intuitive to JS users.

Potential extensions needed for the functions @goodmind mentioned:

type Tpl = [string, number];
type MyTpl = [...Tpl, boolean];

(^ or using a generic Tpl, as above)

@bterlson
Copy link
Member

bterlson commented Jun 12, 2018

I believe that the better choice for the open-ended tuple syntax is [ ... string[] ].

Star has two benefits: arguably better syntax, and preventing generic rest parameters (edit: for now 😝). The latter is solved as @ahejlsberg notes by always requiring the last element to have an array type, which I think is a reasonable restriction.

Syntactically, I guess * looks very foreign to JS developers and that most would prefer a more familiar syntax (this thread is good evidence, thanks everyone!). * also opens up a whole can of worms around which "regexp-type operators" are supported (e.g. can we write [string, string*] as [string+]?). Lastly, if we get full support for variadic types, the ... syntax will have to be supported anyway, and thus we will end up with two syntaxes for the same thing (i.e. [number, string*] and [number, ... string[]] and presumably a lint rule to make one or the other preferred).

I think we should just adopt the javascripty syntax that will be intuitive to those with experience using rest/spread and is a pure subset of existing proposals for variadic types and others.

@ahejlsberg
Copy link
Member Author

With #26676 the type of rest parameter can now be a union of tuple types. This effectively provides a form of overloading expressed in a single function signature.

@kgtkr
Copy link

kgtkr commented Sep 7, 2018

T["length"] example

type Take<N extends number, T extends any[], R extends any[]=[]> = {
  0: Reverse<R>,
  1: Take<N, Tail<T>, Cons<Head<T>, R>>
}[T extends [] ? 0 : R["length"] extends N ? 0 : 1];

export type Group<N extends number, T extends any[], R1 extends any[]=[], R2 extends any[]=[]> = {
  0: Reverse<R2>,
  1: Group<N, T, [], Cons<Reverse<R1>, R2>>,
  2: Group<N, Tail<T>, Cons<Head<T>, R1>, R2>
}[T extends [] ? R1 extends [] ? 0 : 1 : (R1["length"] extends N ? 1 : 2)];

export type Drop<N extends number, T extends any[], R extends any[]=[]> = {
  0: T,
  1: Drop<N, Tail<T>, Cons<Head<T>, R>>
}[T extends [] ? 0 : R["length"] extends N ? 0 : 1];

@jcalz jcalz mentioned this pull request Sep 21, 2018
4 tasks
@trickeyd
Copy link

trickeyd commented Feb 23, 2020

Hello - this fix apparently makes this possible: #24897 but I can't for the life of me figure out how and that thread has been closed now.

I am trying to implement a signal that is used in the following way:

const mySignal = Signal<string, number, SomeType>()
mySignal.emit('sdf', 123, someObj)

So basically emit uses rest arguments that are typed when the signal is created.

Could someone point me in the right direction regarding what the generics for this might look like?

Many thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet