Skip to content

1-tuples seem to be treated the same as arrays #1292

Closed

Description

@Arnavion
function foo<T>(val: [T]) {
    var t = val[0]; // T
    var u = val[1]; // T
}

It seems to be treating [T] (1-tuple of T) as T[]. So u is inferred as T instead of {}


function foo<T>(val: [[T]]) {
    var t = val[0]; // [T]
    var u = val[1]; // [T]
    var v = t[0]; // T
    var w = t[1]; // T
}

It seems to be treating [[T]] (1-tuple of 1-tuple of T) as T[][]. So u and w are both not inferred as {}.


function foo<K, V>(val: [[K, V]]) {
    var t = val[0]; // [K, V]
    var u = t[0]; // K
    var v = t[1]; // V
    var x = t[2]; // {}
    var y = val[1]; // [K, V]
}

It seems to be treating [[K, V]] (1-tuple of 2-tuple of K and V) as [K, V][] (array of 2-tuple of K and V). So y is being inferred as [K, V] instead of {}


This works fine:

function foo<T, U, V, W>(val: [[T, U], [V, W]]) {
    var t = val[0]; // [T, U]
    var u = val[1]; // [V, W]
    var v = t[0]; // T
    var w = t[1]; // U
    var x = t[2]; // {}
    var y = val[2]; // {}
}

so it's not a general issue with tuple of tuples, just 1-tuples.

Activity

  1. DanielRosenwasser commented on Nov 28, 2014

    @DanielRosenwasser
    Member

    This looks to be by design. If you had a[number, number] tuple type, you'd see this same behavior at index 2. It used to be that at an arbitrary or unaccounted index, you'd get the best-common type of the constituent types, which often resulted in {}. In 1.4, for your last example, x will have the type T | U because we'll be using the union type of the constituent types.

    From 3.3.3 in the Spec:

    Specifically, a tuple type

    [ T0, T1, ..., Tn ]

    combines the set of properties

    {  
        0: T0;  
        1: T1;  
        ...  
        n: Tn;  
    }

    with the members of an array type whose element type is the union type (section 3.4) of the tuple element types.

  2. Arnavion commented on Nov 28, 2014

    @Arnavion
    Author

    Hmm. I assumed the intent of inferring {} was specifically to disallow doing anything useful with indices higher than in the tuple signature without casting (and thus expressing the intent that you know what you're doing). I don't think I understand the logic behind giving higher indices the union type of the defined indices - the higher indices are undefined as far as the type system is concerned, so the only options that seem right are compiler error, any, or {}.

    Also, in that case, shouldn't v here would be inferred as B | C (or atleast A) and not as {} ?

    class A { foo: string; }
    class B extends A { bar: string; }
    class C extends A { baz: string; }
    
    var value: [B, C];
    var t = value[0]; // B
    var u = value[1]; // C
    var v = value[2]; // {}
  3. DanielRosenwasser commented on Nov 29, 2014

    @DanielRosenwasser
    Member

    Correct, when union types make it in to an official release, you'll no longer get {} for v. The idea of tuple types is that they tie in closely to ES6 destructuring semantics. ES6 doesn't have tuples, but it does have arrays, and I think we didn't want to limit the expressivity you get with arrays. For instance, a contrived example might be:

    type Point3D = [number, number, number];
    
    function distFromOrigin(point: Point3D) {
        return point.reduce((acc, c) => acc + c * c, 0);
    }
    
    alert(distFromOrigin([3,3,3]));
  4. Arnavion commented on Nov 29, 2014

    @Arnavion
    Author

    Daniel Rosenwasser (@DanielRosenwasser) I was asking why v isn't inferred as A, since in your first post you said it should be inferred as the best common type.

    Your example does make the case for having tuples decay to arrays for the purpose of calling methods, but I still feel that indexer access for tuples should prevent accessing indices higher than in the type signature, either via a compiler error or requiring an explicit downcast from {}.

  5. DanielRosenwasser commented on Nov 29, 2014

    @DanielRosenwasser
    Member

    Sorry, I missed that question.

    Yes, it's a bit odd. That's actually the way best common type was calculated, even in the following context:

    var arr = [{a: 1, b: "string"}, {a: 1, c: true}]; // {}[]

    In 1.4, we will instead use the union type of the contained expressions' types.

  6. ahejlsberg commented on Nov 29, 2014

    @ahejlsberg
    Member

    Arnav Singh (@Arnavion) In your example:

    class A { foo: string; }
    class B extends A { bar: string; }
    class C extends A { baz: string; }
    
    var value: [B, C];
    var t = value[0]; // B
    var u = value[1]; // C
    var v = value[2]; // B | C

    the type of v is indeed inferred as B | C, and not as {}. This is with the latest compiler in master and it is the intended behavior.

    The reason we use the union type for elements outside the declared tuple range is code like this:

    var t = value[0]; // B
    var u = value[1]; // C
    var i = condition ? 0 : 1;
    var v = value[i]; // B | C
    

    We could consider giving an error when the index is known to be out of range (i.e. in the isolated case of an out of range numeric literal), but I'm not sure it is worth the effort. As an aside, we will report an error with destructuring, i.e.

    var value: [B, C];
    var [t, u] = value;  // Ok, t: B and u: C
    var [t, u, v] = value;  // Error, no matching value for 'v'
    

    The destructuring work is slated for the 1.5 release.

  7. Arnavion commented on Nov 29, 2014

    @Arnavion
    Author

    Since TS considers m-tuples to be assignable to n-tuples if n < m and the first n types of both are the same, one can write code like this:

    class A { foo: string; }
    
    var tuple1: [number, string]
    var tuple2: [number, string, A];
    
    function bar(tuple: [number, string]) {
        var t = tuple[2];
        if (t !== undefined) { /* we were passed a 3-tuple, do things with t */ }
    }
    
    bar(tuple1);
    bar(tuple2);

    If t is inferred as number | string it will become a runtime problem when calling bar with tuple2, so it would be nice to have it not compile as-is without even a warning. Please consider either a compiler warning / error or changing the inference rules in such a way that it requires an explicit cast to be able to do anything useful with t.

    It is good that destructuring assignment is going to give an error on accessing higher indices than the tuple signature. But then users will surely be confused at the inconsistency between destructuring a tuple automatically (conservative, default is type-safe) and manually (default is type-unsafe).


    Incidentally, I thought the way to make my above example type-safe might be to change the signature to take in the 3-tuple I want, and add a 2-tuple overload.

    class B { bar: { baz: string } };
    
    function bar(tuple: [number, string]);
    function bar(tuple: [number, string, B]) {
        var t = tuple[2];
        if (t !== undefined) {
            // Being called via 3-tuple overload
            console.log(t.bar.baz);
        }
    }

    I expected the call to bar(tuple2) to not compile since B is not assignable to A, but it ends up being compiled against the 2-tuple overload. So the t.bar.baz access would blow up at runtime.

  8. ahejlsberg commented on Nov 29, 2014

    @ahejlsberg
    Member

    Arnav Singh (@Arnavion) Your example:

    class A { foo: string; }
    
    var tuple1: [number, string]
    var tuple2: [number, string, A];
    
    function bar(tuple: [number, string]) {
        var t = tuple[2];
        if (t !== undefined) { /* we were passed a 3-tuple, do things with t */ }
    }
    
    bar(tuple1);
    bar(tuple2);  // Error

    does indeed produce an error because string | number | A (the type of tuple2's numeric index signature) is not assignable to string | number (the type of tuple1's numeric index signature). It only works as long as the extra elements in tuple2 are strings or numbers.

    Again, this is with the latest compiler in master. Are you using a different one?

  9. Arnavion commented on Nov 29, 2014

    @Arnavion
    Author

    I was using the playground. I assumed the only difference between master and 1.3 was the inferered type of the third tuple member, not the assignability of tuples itself. Happy to be shown wrong!

  10. Arnavion commented on Nov 30, 2014

    @Arnavion
    Author

    I'm not entirely convinced it makes sense to infer higher indices than in the tuple signature as a union of the types of the other indices (and in the 1-tuple case leading to the results in the OP). Especially since destructuring is not going to behave the same way.

    But since the master compiler does not allow assigning larger tuples to smaller ones in the type-unsafe cases, I guess this design is here to stay. I'll close this issue.

  11. locked and limited conversation to collaborators on Jun 18, 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

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions