Typescript: Tuple types and map

Created on 22 Jan 2016  路  14Comments  路  Source: microsoft/TypeScript

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.

Fixed Suggestion

Most helpful comment

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.

All 14 comments

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

@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"...)

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?

@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.)

@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.

@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.

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

@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.)

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

See also #3369

@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.)

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..

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.

@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.

Was this page helpful?
0 / 5 - 0 ratings