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

Assume arity of tuples when declared as literal #24350

Open
3 of 4 tasks
aboyton opened this issue May 23, 2018 · 13 comments
Open
3 of 4 tasks

Assume arity of tuples when declared as literal #24350

aboyton opened this issue May 23, 2018 · 13 comments
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript

Comments

@aboyton
Copy link

aboyton commented May 23, 2018

Search Terms

tuples, length, arity

Suggestion

Now that #17765 is out I'm curious about if we could change the arity of tuples declared as literals. This was proposed as part of #16896 but I thought it might be better to pull this part out to have a discussion about this part of that proposal.

With fixed length tuples TypeScript allows you to convert [number, number] to number[] but not the other way around (which is great).

const foo = [1, 2];
const bar = [1, 2] as [number, number];

const foo2: [number, number] = foo;
const bar2: number[] = bar;

If you declare a constant such as foo above it would be nice if it would be nice if it would have the length as part of its type, that is, if foo was assumed to be of type [number, number] not number[].

This would have potential issues with mutable arrays allowing you to call push and pop although this isn't different to present and was discussed a bit in #6229.

const foo = [1, 2] as [number, number]; // After this proposal TypeScript would infer the type as `[number, number]` not `number[]`.
foo.push(3); // foo is of type `[number, number]` even though it now has 3 elements.
foo.splice(2); // And now it has 2 elements again.

Use Cases

When using a function such as fromPairs from lodash it requires that the type is a list of tuples. A simplified version is

function fromPairs<T>(values: [PropertyKey, T][]): { [key: string]: T } {
  return values.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
}

If I do

const foo = fromPairs(Object.entries({ a: 1, b: 2 }));

it works because the type passed into fromPairs is [string, number][], but if I try to say map the values to double their value I get a compile error:

const bar = fromPairs(Object.entries({ a: 1, b: 2 }).map(([key, value]) => [key, value * 2]));

as the parameter is of type (string | number)[][]

This can be fixed by going

const bar = fromPairs(Object.entries({ a: 1, b: 2 }).map(([key, value]) => [key, value * 2] as [string, number]));

but this is cumbersome.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)
@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels May 23, 2018
@mhegazy
Copy link
Contributor

mhegazy commented May 23, 2018

We did consider this when we were first adding support for tuples, and thought it was too big of a breaking change to add.. there are a few issues already open proposing new syntax to address this: #16656 and #10195.

having said that, i would like to bring this back to discussion. i gave it a try today, and there were not that many breaks in our Real World Code, which is frankly surprising to me.. they fall into two categories:

var x = [1, 2];
x = array;  // Error array is not assignable to [number, number]
type A = { foo: string, baz: string[]} ;
var a = <A> { baz: ["baz"] };  // type assertion now fails
var c = [[1, 2]].concat([[3]]); // [[number]] is not assignable to [number,number] | ConcatArray<[number, number]>

@mpawelski
Copy link

I think this would be a good change. Tuples now fell a bit unhandy and with this change people could actually use them more. And with #24897 tuples become even more useful.

But i think automatic code fixes for some errors described by @mhegazy would be necessary (plus of course a flag to get old behavior - as it's done for other breaking changes)

@csvn
Copy link

csvn commented Jun 26, 2018

@mhegazy In the middle example above, it should work if you instead do var a: A = { baz: ["baz"] }; right? In my head, [string] should be assignable to string[].

I frequently use tuples inside e.g. array Array.prototype.map, and having to specify the type via as [T, U] can be very annoying for non-primitive types (see example below). I cannot think of many places I would prefer to have [string, string] widening to string[], but I can think of many places for the opposite. Many cases involve where there are multiple types in the array, e.g. where [number, string, boolean] turns into (number | string | boolean)[].

declare const names: string[];

names
  .map((name, i) => [i, name]) // type widens to (number | string)[]
  .filter(...)
  .forEach(([i, name]) => {
    // i: string | number
    // name: string | number
  });

@mhegazy
Copy link
Contributor

mhegazy commented Jun 26, 2018

In the middle example above, it should work if you instead do var a: A = { baz: ["baz"] }; right? In my head, [string] should be assignable to string[].

Yes; and [string] is assignable to string[]. The problem is neither { foo: string, baz: string[]} is assignable to { baz: [string] } since string[] is not assignable to [string] nor is { baz: [string] } is assignable to { foo: string, baz: string[]} since it does not have the required property foo.

@akdor1154
Copy link

akdor1154 commented Jul 9, 2018

This would be a godsend for people using typescript in jsdoc mode.
I can grumble and live with a few instances of

Object.entries(obj)
  .map([k, v] => [k, k.toLowerCase()] as [string, string]

, but a few chained hits of the jsdoc equivalent

Object.entries(obj)
  .map([k, v] => /** @type {[string, string]} */ ([k, k.toLowerCase()])

just makes me want to turn off my 'lint with tsc' step, not to mention irritates the gonads off anyone on my project who is not a typescript native.

@RyanCavanaugh RyanCavanaugh added Revisit An issue worth coming back to and removed In Discussion Not yet reached consensus labels Aug 23, 2018
@ChiriVulpes
Copy link

ChiriVulpes commented Aug 25, 2018

I would really like this to be a thing, but until it is, with the new rich tuple support there's a simple workaround.

function tuple<T extends any[]>(...items: T): T {
	return items;
}
// old version
Enums.values(TileTemplateType)
	.map<[TileTemplateType, TranslationGenerator]>(type => [type, Translation.generator(TileTemplateType[type])])
	.collect(Collectors.toArray)
	.sort(([, t1], [, t2]) => Text.toString(t1).localeCompare(Text.toString(t2)))
	.values()
	.map<IDropdownOption<TileTemplateType>>(([id, t]) => [id, (option: Button) => option.setText(t)])

// new version
Enums.values(TileTemplateType)
	.map(type => tuple(type, Translation.generator(TileTemplateType[type])))
	.collect(Collectors.toArray)
	.sort(([, t1], [, t2]) => Text.toString(t1).localeCompare(Text.toString(t2)))
	.values()
	.map(([id, t]) => tuple(id, (option: Button) => option.setText(t)))

Hope this helps somebody!

P.S. .......add...a.....flag? 😉

@RyanCavanaugh
Copy link
Member

P.S. .......add...a.....flag? 😉

I WILL TURN THIS CAR AROUND

@aboyton
Copy link
Author

aboyton commented Jan 31, 2019

It seems like #29510 might help for this case? I haven't tested it out yet but here's to hoping.

@ChiriVulpes
Copy link

@aboyton Even if it does solve the same issue, it's still a workaround, and it requires 2-4 more characters than the tuple function workaround (<> or _as_). Unfortunately this issue will remain relevant.

@MattiasMartens
Copy link

I've bumped into this problem more times than I would like:

function tupler(): [number, string] {
  const ret = [3, "C"];

  return ret;
}

Error: Type '(string | number)[]' is missing the following properties from type '[number, string]': 0, 1

Yes, it can be fixed with a cast, but also, why isn't it enough to specify the enclosing function's return type?

@ChiriVulpes
Copy link

@MattiasMartens

When storing a tuple in a variable like that, the type is set to (string | number)[], and that's never assignable to [number, string]. The only reason it works to return [3, "C"] directly (rather than first storing it in a variable) is that in that case the type checker is then applied to the [3, "C"] directly, which is assignable to [number, string].

This could be a thing, by adding in logic that allowed variables which are created and then immediately returned to not get that default (string | number)[] type, but in that case that's a separate feature request.

At least, I think this is how it works. Someone correct me if I'm wrong.

@zakjan
Copy link

zakjan commented Feb 5, 2019

Generally I try to avoid tuples (because it's not clear later what meaning has values inside), but there is one native usecase, where unfortunately there is no other option: constructing Maps.

const nums = [1, 2, 3];

new Map(nums.map(num => [num, num * 2])); // error, ideally should work

new Map(nums.map((num): [number, number] => [num, num * 2])); // works
new Map(nums.map(num => [num, num * 2] as [number, number])); // works

@KisaragiEffective
Copy link

Bump: IMO doing flow analysis on variables whose declared with const and its initializer is tuple can solve this issue. However, I'm worrying about increasing time and if it's in the TS scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

10 participants