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

Tuple types and map #6574

Closed
elbarzil opened this issue Jan 22, 2016 · 14 comments · Fixed by #11252
Closed

Tuple types and map #6574

elbarzil opened this issue Jan 22, 2016 · 14 comments · Fixed by #11252
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@elbarzil
Copy link

I have a piece of code with many foo[]s that should really be [foo,foo] -- it would be nicer if map had a type that preserves tuples, so something like this:

var x: [number, number];
x = [1,2].map(x=>x+1);

would work, even if it's some hack with a union of tuples up to some number of elements (I think that I saw something like that around rest arguments, and I imagine that this in a close neighborhood).

I thought that this would be common, but didn't find anything about it. Feel free to dismiss if it is.

@DanielRosenwasser DanielRosenwasser 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 Jan 22, 2016
@aluanhaddad
Copy link
Contributor

@elbarzil I assume you are only proposing this for heterogeneous tuples. Is that correct?

@elbarzil
Copy link
Author

@aluanhaddad, I'm just talking about having .map preserve the tuple-ness of its input, instead of losing it. That should work regardless of the all of the tuple types being the same or not. Eg,

var x: [string, number];
x = ["one",2].map(x=>x);

(Or maybe I misunderstand what you mean by "heterogeneous tuples"...)

@kitsonk
Copy link
Contributor

kitsonk commented Mar 11, 2016

Map often mutates the types (and is usually the primary purpose of a map)... heterogeneous tuples would be tuples of all the same type (e.g. [ string, string ] or [ number, number, number ]). I suspect @aluanhaddad suggested it because it would be challenging at design time to interpret the dynamic nature of the return type, so whatever the return type of the function would be, would then populate all the types of each member of the tuple.

Based on your example, how would you propose handling something like this?

let x: [ string, number ];
let y = [ '1', 2 ].map(x => Number(x)); // what is the type of y?

@elbarzil
Copy link
Author

@kitsonk, several things I'm confused with: (a) the common meaning of "heterogeneous" in the context of types is different types, especially in lists/arrays/etc where they're contrasted with homogeneous containers where all items have the same type. (b) I'm also confused by "mutates the types" -- types are never mutated, so I don't know what that sentence means... (c) And in your code snip I don't know what the purpose of the first line...

But overall, there shouldn't be any particular problem here: you know how to apply a function on elements of an array one by one and deal with the types, and the same logic should apply for using .map(). To make this more explicitly detailed, I want this code to work (shown with Number() too)

function id<T>(x: T): T { return x; }
var x: [string, number];
x = [id("one"), id(2)];
x = ["one", 2].map(id); // should be the same wrt types
// similarly:
var y: [number, number];
y = [Number("1"), Number(2)];
y = ["1", 2].map(Number); // again

where each of the .map assignments get typed in the same way as the preceding lines. Here's rephrasing this with a variable for an input, to make the type it gets stated explicitly:

function id<T>(x: T): T { return x; }
var a: [string, number] = ["one", 2];
var x: [string, number];
x = [id(a[0]), id(a[1])];
x = a.map(id); // should be the same wrt types
// similarly:
var b: [string, number] = ["1", 2];
var y: [number, number];
y = [Number(b[0]), Number(b[1])];
y = b.map(Number); // again

And finally, another variation:

function id<T>(x: T): T { return x; }
function map2<T1,T2>(f: (x:T1)=>T2, x: [T1,T1]): [T2,T2] {
    return [f(x[0]), f(x[1])];
}
var x: [string, number];
x = [id("one"),id(2)];
x = map2(id, ["one",2]);
var y: [number, number];
y = [Number("1"), Number(2)];
y = map2(Number, ["1", 2]);

which is supposed to show that I can do this manually, and it almost works except that in the first map2 call TS decided to infer {} as the types for T1 and T2. (That seems like some additional problem.)

@aluanhaddad
Copy link
Contributor

@kitsonk Indeed, that was precisely what I was getting at.
Edit: actually I think you meant homogeneous.

@elbarzil If the tuple elements are of the same type, the result of mapping a function over the tuple would always produce a tuple with homogeneous elements. I don't know that changing the result of

(x as [T, T]).map(f)

from U[] to [U, U] would add much value.

Now, in the case of heterogeneous tuples, there are a few interesting use cases I can think of.

class A {}
class B {}
let myAB = [new A(), new B()]; // [A, B]
let myFrozenAB = myAB.map(Object.freeze); // [A, B]

That is interesting, but still not that useful, as their are perfectly clean alternative ways to express it.

Another use case would be if the function mapped over the tuple has several type specific overloads declared.

@elibarzilay
Copy link
Contributor

@aluanhaddad, sure it does add value! (Otherwise I wouldn't start this in the first place.)

Consider a library that deals with 2d points represented as [x,y] -- the types should all be a pair of two numbers, and preserving that property (by making the typechecker enforce it) adds the value of less chances of bugs. Should I be allowed to concatenate two such points? .push() a new number to a point? .pop() one out? etc? -- Yes, but the result will not be a valid point, unlike .map() with a numeric function.

That's a classic use of a typechecker like TS, and it already has a way to represent this information, it just doesn't get maintained in some places.

@FranklinWhale
Copy link

I think there can be an improvement in the type inference system:

let x = [0, 1]; // Inferred to be number[]
let y: number[] = x; // OK
let z: [number, number] = x; // Fails

@elibarzilay
Copy link
Contributor

@FranklinWhale, that's related -- though I know that it's sometimes tricky for these languages to find the "most expected" type. But in my case I was willing to add the type annotations, but map would just lose them anyway...

(I agree that it seems better to have it infer a [number,number] in the above case, since it's a subtype of number[] anyway, but it might be tricky if there are assignments also, since then you're forbidding things like adding a number to x so there might not be a good way to require a type declaration for this.)

@FranklinWhale
Copy link

@elibarzilay: I have just made a related reply in #7799 :)

@FranklinWhale
Copy link

See also #3369

@elibarzilay
Copy link
Contributor

@FranklinWhale, ah yes, #3369 has the problem I mentioned...

(And FWIW, dealing with assignment in a type system with subtypes can make things very complicated in a way that is not obvious. I have a whole section of class notes about the problems it leads to, and it's not really trivial, or expected. The bottom line is that a type of T for a writable variable makes it neither a subtype nor a supertype of any other writable type except for T itself.)

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Apr 17, 2016

Consider a library that deals with 2d points represented as [x,y] -- the types should all be a pair of two numbers, and preserving that property (by making the typechecker enforce it) adds the value of less chances of bugs. Should I be allowed to concatenate two such points? .push() a new number to a point? .pop() one out? etc? -- Yes, but the result will not be a valid point, unlike .map() with a numeric function.

I'm not convinced that using a tuple is a good way to represent a 2d point or generally a vectorn for some n. Applications of these types will likely want to have specific functionality such as addition, subtraction, magnitude, scaling, dot product, cross product (where defined for n), etc..

@dbrgn
Copy link

dbrgn commented Jul 26, 2016

Here's another case for tuple types:

let values = [{id: 1, name: 'anna'}, {id: 2, name: 'bob'}];
let map: Map<number, Object> = new Map(values.map(x => [x.id, x]));

This does not currently work.

@Jessidhia
Copy link

Jessidhia commented Oct 25, 2017

@dbrgn your case is actually one of the easiest to work around (in newer TS versions) in a sound manner; despite being a bit verbose:

const values = [{id: 1, name: 'anna'}, {id: 2, name: 'bob'}]
const map = new Map(values.map(x => [x.id, x] as [typeof x.id, typeof x]))
// map is Map<number, { id: number; name: string; }>

TS has a tuple type; it just prefers to never infer anything as a tuple.

@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

Successfully merging a pull request may close this issue.

8 participants