Skip to content

Tuple types and map #6574

Closed
Closed

Description

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.

Activity

  1. aluanhaddad commented on Mar 11, 2016

    @aluanhaddad
    Contributor

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

  2. elbarzil commented on Mar 11, 2016

    @elbarzil
    Author

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

  3. kitsonk commented on Mar 11, 2016

    @kitsonk
    Contributor

    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 Aluan Haddad (@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?
  4. elbarzil commented on Mar 11, 2016

    @elbarzil
    Author

    Kitson Kelly (@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.)

  5. aluanhaddad commented on Mar 26, 2016

    @aluanhaddad
    Contributor

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

    Eli Barzilay (@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.

  6. elibarzilay commented on Mar 27, 2016

    @elibarzilay
    Contributor

    Aluan Haddad (@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.

  7. FranklinWhale commented on Apr 4, 2016

    @FranklinWhale

    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
  8. elibarzilay commented on Apr 4, 2016

    @elibarzilay
    Contributor

    Franklin Tse (@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.)

  9. FranklinWhale commented on Apr 4, 2016

    @FranklinWhale

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

  10. FranklinWhale commented on Apr 4, 2016

    @FranklinWhale

    See also #3369

  11. elibarzilay commented on Apr 4, 2016

    @elibarzilay
    Contributor

    Franklin Tse (@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.)

  12. aluanhaddad commented on Apr 17, 2016

    @aluanhaddad
    Contributor

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

  13. dbrgn commented on Jul 26, 2016

    @dbrgn

    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.

  14. added
    FixedA PR has been merged for this issue
    and removed
    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.
    on Oct 4, 2016
  15. Jessidhia commented on Oct 25, 2017

    @Jessidhia

    Danilo Bargen (@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.

  16. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    FixedA PR has been merged for this issueSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions