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

Conversation

Projects
None yet
@ahejlsberg
Member

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

This comment has been minimized.

Contributor

Kingwl commented Jun 12, 2018

thanks for your great working🤩

@felixfbecker

This comment has been minimized.

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] = 'typescript@2.9.0'.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

This comment has been minimized.

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

This comment has been minimized.

Cryrivers commented Jun 12, 2018

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

This comment has been minimized.

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

This comment has been minimized.

goodmind commented Jun 12, 2018

But tuples can't be infinite by definition?

Merge branch 'master' into restTuples
# Conflicts:
#	tests/baselines/reference/api/tsserverlibrary.d.ts
#	tests/baselines/reference/api/typescript.d.ts

@goodmind goodmind referenced this pull request Jun 12, 2018

Open

operate on tuples #17

@j-oliveras

This comment has been minimized.

Contributor

j-oliveras commented Jun 12, 2018

@j-oliveras j-oliveras referenced this pull request Jun 12, 2018

Closed

Allow use of spread operator in tuple types #24902

4 of 4 tasks complete
@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented Jun 12, 2018

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

@goodmind

This comment has been minimized.

goodmind commented Jun 12, 2018

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

@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented Jun 12, 2018

@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

This comment has been minimized.

felixfbecker commented Jun 12, 2018

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

@tycho01

This comment has been minimized.

Contributor

tycho01 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:

  • generic support (the ...X mentioned by @ahejlsberg)
  • expanding tuple types in tuple type literals (#17884) (edit: same as Concat, defined by @fightingcat below!):
type Tpl = [string, number];
type MyTpl = [...Tpl, boolean];

(^ or using a generic Tpl, as above)

@bterlson

This comment has been minimized.

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.

@restrry restrry referenced this pull request Jun 12, 2018

Closed

Typescript args issue #1482

@dbkaplun

This comment has been minimized.

dbkaplun commented Jul 15, 2018

Is it possible to use this today, for example using a git URL in package.json?

@wesleyolis

This comment has been minimized.

wesleyolis commented Jul 15, 2018

This is an additional feature request that builds on functionality here:
#25670

@kgtkr

This comment has been minimized.

kgtkr commented Aug 9, 2018

type Zip<A extends any[], B extends any[], R extends any[]=[]> = {
    0: Reverse<R>,
    1: Zip<Tail<A>, Tail<B>, Cons<[Head<A>, Head<B>], R>>
}[A extends [] ? 0 : B extends [] ? 0 : 1];
@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented Aug 25, 2018

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

This comment has been minimized.

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];

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