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

Feature Request: Tuples with variable type arguments #1336

Closed
JeroMiya opened this issue Dec 2, 2014 · 16 comments
Closed

Feature Request: Tuples with variable type arguments #1336

JeroMiya opened this issue Dec 2, 2014 · 16 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@JeroMiya
Copy link

JeroMiya commented Dec 2, 2014

Motivation:
Some JavaScript APIs take arguments of type Array, where the array is constructed, for example, by taking a number of items of one type followed by one or more arguments of another type, or perhaps the other way around. A real world example of this is the array-based dependency injection functionality in AngularJS, where you pass in an array consisting of a variable number of strings, followed by a function.

Motivating real world example:

angular.module('app').controller(['$scope', function($scope: ng.IScope) { /*etc...*/ }]);

// how we might type this in 1.3:
interface IModule {
  controller(injectable: () => any);
  controller(injectable: [string, () => any]);
  controller(injectable: [string, string, () => any]);
  controller(injectable: [string, string, string, () => any]);
  controller(injectable: [string, string, string, string, () => any]);
  // and so on, until we have 'enough' overloads to cover common usage
}

Suggestion:
Enhancement to the Tuple type annotation capability to support variable numbers of type arguments to support the above scenario without a fixed set of overloads.

Example tuple typing:

// same as controller function in the 1.3 example above
controller(injectable: () => any): void;
controller(injectable: [...string, () => any]): void {
  // type of expression injectable[n] is string | () => any
  // MAYBE special case for known indices: injectable[0] is type string and/or injectable[injection.length - 1] is () => any
}

// generalized examples (I don't have real-world examples of these, but might as well be comprehensive)
function foo1(variableLast: [() => any, ...string]) {
  // type of expression variableLast[0] is () => any
  // type of expression variableLast[n] is () => any | string
}
function foo2(variableFirst: [...string, () => any]) {}

// these forms might be possible, but doubtful there are real-world examples
// of these in JS apis, and would be bad practice in general. I would be OK if these
// forms were not allowed:
function foo3(variableMiddle: [string, ...number, () => any]) {
  // type of expression variableMiddle[0] is string
  // type of expression variable[n] is string | number | () => any
}
function foo4(twoVariables: [...string, ...number]) {}
function foo5(fixedInMiddleOfVariables: [...string, number, ...string]) {}

Edited: moved the ... token to the left of the type reference to more closely match the variable function argument syntax.

Edited 12/3/2014: examples foo3, foo4, and foo5 would be possible, but not likely to be used and bad practice. Documented this and separated them out from the first two examples, which are the real motivating ones.

Additional note: syncing function arguments with the number of type arguments in a tuple type is beyond the scope of this suggestion.

@mihailik
Copy link
Contributor

mihailik commented Dec 2, 2014

Looks sensible. The obvious caution is how is this feature overlaps with varargs, when the variable-size argument is the one typed in this var-tuple way.

@JeroMiya
Copy link
Author

JeroMiya commented Dec 2, 2014

Good point. I think that works out syntactically - this new syntax is bounded within the tuple annotation:

function foo(...injectables: [...string, (...args: any) => any]) {}

The issue is likely to be the inability to implement the same flexibility with function varargs as you can with variable tuple type arguments, because the function varargs are referencable individually:

// this we can't do, but we could
function example1(...arg1: string, arg2: number, arg3: string) {
}
// translation to JS:
function example1() {
  var arg1 = [];
  var arg2 = arguments[arguments.length - 2];
  var arg3 = arguments[arguments.length - 1];
  for(var _i = 0; _i < arguments.length - 2; _i++) {
    arg1[_i] = arguments[_i];
  }
}

// this we can do now
function example2(arg1: string, ...arg2: string) {}

// this we cannot do.
function example3(...arg1: string, ...arg2: number) {
  // what does the expression arg2[n] translate to? arg2.length? no way to tell at runtime
}

There might be a workaround if you could somehow specify varargs as a tuple instead of splats, but this could get confusing. Or you could just live with tuples being more flexible than varargs. Or, you could extend varargs to enable example1 above, and not allow foo3, foo4, or foo5 above, so that they're consistent.

Edit: updated example1 to match what the compiler currently does for varargs, slightly modified to support varargs at the beginning and fixed args at the end.

@NN---
Copy link

NN--- commented Feb 25, 2015

I have use case where array is typed differently for different indexes.

I would like to have this sample.
First item is a number and the rest items are objects with property 'prop'

class A {
 a: [x: number, ...rest: { prop: number; }] = [1, {prop:2}, {prop:3}];
}

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Mar 4, 2015
@RyanCavanaugh
Copy link
Member

Leaning toward "too complex" here unless there are more compelling examples than Angular's wacky DI setup. Might be worth discussing if we had a proof-of-concept PR that didn't introduce too much complexity.

@jods4
Copy link

jods4 commented Oct 12, 2015

@RyanCavanaugh what about the unlimited number of arguments passed to Promise.all. This example comes from a discussion in #1664.
Promise.all<X,Y,Z>([Promise<X>|X, Promise<Y>|Y, Promise<Z>|Z]): [X,Y,Z]

@saschanaz
Copy link
Contributor

@jods4 That may also be related with #1773. With arbitrary syntax:

interface PromiseConstructor {
    all<...T[]>(...values: (T | PromiseLike<T>)[]): Promise<...T[]>
}

@panuhorsmalahti
Copy link

This would be useful when e.g. defining a table row like this:

const rows: [string, number, ..rest: any][] = []

const ID = 123;
rows.push(["My data", ID, prop1, prop2, prop3, prop4]);

@jameskeane
Copy link
Contributor

@saschanaz Promise.all takes an Iterable. How would you suggest modifying your sample, assuming you were intending to specify rest parameters?

The following is similar to parameter packing in C++

module Promise {
  function all<...T_values>(
      values: [ (<T|Thenable<T>> T_values...) ]  // unpack, cast, then repack to tuple ?
  ): Promise<T_values> // keep it packed, assuming the type is a tuple of whatever types
}

Maybe now you see @RyanCavanaugh's point, about how complicated this can get. What other transformations will need to be supported?

Say I want to implement Array.flatten with support for variadic tuples, so we need to accept a variadic number of variadically typed tuples, then return a single variadic tuple?

How deep is the rabbit hole?

@saschanaz
Copy link
Contributor

@jameskeane You may want to continue discussion here: #5453

@masaeedu
Copy link
Contributor

@RyanCavanaugh Most of the RxJS API is similarly typed. See for an example the concat operator. Notice the scheduler argument they have to pass at the end. They ended up having to write their own tooling for regenerating all the overloaded type definitions in their source files every time they make a change, and it still isn't properly statically typed (they're forced to cop out and use a union of the observable types and the scheduler as the return type, which means discipline and understanding of the API is required on the part of the library consumer).

Unfortunately most JS libraries out there don't really care about the wackiness of this pattern in a statically typed world. The brunt is borne by developers of TypeScript definitions, who usually don't have a full team of contributors available to write their own tooling for generating lots of overloads. A typical developer writing typings just wants to crank something out in 10 minutes and be using their JS library. If things remain this painful on this front, it affects everyone consuming community typings.

I came across this issue as a result of @unional asking on the typings gitter how he would represent the following function signature:

(...files: string[], handler: Function): void

The proposal above would have allowed him to represent this type signature very conveniently. The current state of affairs requires him to write lots of overloads, or sacrifice some type safety and use the misleading (...files: (string|Function)[]): void, or abandon all static typing and use any[].

@jayphelps
Copy link

@masaeedu FYI #7347

@masaeedu
Copy link
Contributor

@jayphelps Thanks, that is informative. FWIW, the proposal here is only syntactically different from what you proposed in that issue:

function concat(...observables: Array<Observable>, scheduler: Scheduler);
function concat(...args: Array<Observable | Scheduler>) {
  console.log(args.length);
  // 3
}

concat(new Observable, new Observable, new Scheduler);

Within the concrete implementation the spread parameter is only available as a union type (this is not a problem because a) the implementation usually isn't even in TypeScript, and b) flow analysis is great and constantly improving). The important thing is exposing a well typed API for the consumer, which is very painful right now.

@Igorbek
Copy link
Contributor

Igorbek commented Sep 30, 2016

You may also be interested in proposal #6229

@RyanCavanaugh
Copy link
Member

Rest tuples are much more expressive now and handle this as well as we can. You'll need to use the ...args: tuple syntax, but all these use cases are broadly solvable now.

@ChuckJonas
Copy link

@RyanCavanaugh can you provide an example of the classic "variable number of args followed by a callback"?

EG: #1360

@IllusionMH
Copy link
Contributor

See examples from initial post #39094 and comment below

Callback last is

// Inferring parts of tuple types

declare function foo<T extends string[], U>(...args: [...T, () => void]): T;

foo(() => {});  // []
foo('hello', 'world', () => {});  // ["hello", "world"]
foo('hello', 42, () => {});  // Error, number not assignable to string

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests