Skip to content

ES6 Map array constructor needs extra hints #8936

Closed

Description

TypeScript Version:

1.8.10

Code

        const iAmAnArray [
        { value: "value1", text: "hello" }
        { value: "value2", text: "map" }
        ];
        const iAmAMap = new Map<string, string>(
            iAmAnArray.map(x => [x.value, x.text])
        );

Expected behavior:
I would expect this to be working code.

Actual behavior:
It errors with:

[ts] Argument of type 'string[][]' is not assignable to parameter of type 'Iterable<[string, string]>'.
  Types of property '[Symbol.iterator]' are incompatible.
    Type '() => IterableIterator<string[]>' is not assignable to type '() => Iterator<[string, string]>'.
      Type 'IterableIterator<string[]>' is not assignable to type 'Iterator<[string, string]>'.
        Types of property 'next' are incompatible.
          Type '(value?: any) => IteratorResult<string[]>' is not assignable to type '(value?: any) => IteratorResult<[string, string]>'.
            Type 'IteratorResult<string[]>' is not assignable to type 'IteratorResult<[string, string]>'.
              Type 'string[]' is not assignable to type '[string, string]'.
                Property '0' is missing in type 'string[]'.

If I give the compiler a clue using as [string, string] this becomes working code:

        const iAmAnArray [
        { value: "value1", text: "hello" }
        { value: "value2", text: "map" }
        ];
        const iAmAMap = new Map<string, string>(
            iAmAnArray.map(x => [x.value, x.text] as [string, string])
        );

Is there a reason that we have to tell the compiler specifically what we we're pushing out? (ie an array with 2 entries)

I can do this but it's not obvious; I've found no documentation anywhere on this.

Activity

  1. kitsonk commented on Jun 2, 2016

    @kitsonk
    Contributor

    What you are referring to is a tuple. Tuples, because they are a construct of TypeScript only, cannot be contextually inferred, because they are ambiguous and TypeScript will default to the most "JavaScripty" type. For example, at run-time, these two things look exactly the same:

    const stringTuple: [ string, string ] = [ 'foo', 'bar' ];
    const stringArray: string[] = [ 'foo', 'bar' ];
    const stringContextual = [ 'foo', 'bar' ]; // infers, string[]

    I am sure the TypeScript team would be glad to accept a PR on the documentation.

  2. johnnyreilly commented on Jun 2, 2016

    @johnnyreilly
    Author

    Thanks for commenting Kitson Kelly (@kitsonk). Due to the syntax similarity I hadn't made the distinction between arrays and tuples in this context. That's very helpful.

    I'm not aware of any documentation of TypeScript Map usage; do you happen to know of any? I've blogged about this for now so the information is out there (and here obviously 😄).

    I'd be interested to know if there's any possibility that the compiler could be made to infer a tuple in the case when a fixed size array is known to be exported from a function (as in this case)...

  3. ivogabe commented on Jun 2, 2016

    @ivogabe
    Contributor

    TypeScript should be able to infer a tuple type in the Map constructor. Might #8407 be related?

  4. johnnyreilly commented on Jun 2, 2016

    @johnnyreilly
    Author

    Oh that's interesting Ivo Gabe de Wolff (@ivogabe); it does look related but still different I think... I'm not clear the PR will allow TypeScript to infer a tuple type from an array; I stand to be corrected of course...

  5. ivogabe commented on Jun 2, 2016

    @ivogabe
    Contributor

    TypeScript already could infer tuple types, but you need to 'trigger' it. So when you pass an array to [T, U], it will try to make a tuple type. The problem in the issue I referenced was that the signature of Map didn't trigger there. The signature uses an iterable, and that isn't considered when the target is ES5. The fix was to add a new signature to it, which accepts an array instead of an iterable.

  6. johnnyreilly commented on Jun 2, 2016

    @johnnyreilly
    Author

    So you think the PR will resolve this? Or is that only in the case where the target is ES5? (I'm targeting ES6)

  7. ivogabe commented on Jun 2, 2016

    @ivogabe
    Contributor

    Oh nvm, this is a different issue. The problem is here that the inference does not work through function calls. So TS isn't triggered to create a union type in map. You could change your code to this:

            const iAmAMap = new Map(
                iAmAnArray.map<[string, string]>(x => [x.value, x.text])
            );
  8. kitsonk commented on Jun 2, 2016

    @kitsonk
    Contributor

    Yes, I think there is a level of indirection there where TypeScript has to "bubble up" the inference of the types given the call site and where they are ambiguous it picks the more "likely" one. So string[] wins out over [ string, string ] because contextually, it is less "surprising":

    function foo() {
      return [ 'foo', 'bar' ];
    }
    
    const a = foo(); // string[] is less surprising than [ string, string ]
    

    Because what are you intending? There are no contextual clues that you meant for that array to be fixed at only two elements. The only thing that is "safe" for TypeScript to assume is that it is an array of strings. There has to be a limit to how much guessing TypeScript can do on your behalf.

  9. johnnyreilly commented on Jun 2, 2016

    @johnnyreilly
    Author

    Cheers for the alternate syntax Ivo Gabe de Wolff (@ivogabe).

  10. mhegazy commented on Jun 2, 2016

    @mhegazy
    Contributor

    As noted by Ivo Gabe de Wolff (@ivogabe) and Kitson Kelly (@kitsonk) tuple types are never inferred without a contextual type.

  11. ivogabe commented on Jun 2, 2016

    @ivogabe
    Contributor

    Mohamed Hegazy (@mhegazy) I don't know how complex it would be, but it would be really cool if contextual typing would work in such case too, as it would be useful for functional programming in TypeScript. Would that be doable? (I might have written too much Haskell lately).

  12. mhegazy commented on Jun 2, 2016

    @mhegazy
    Contributor

    it is a chicken and egg problem. we need the type to select the overload, that has the contextual type. ideally you would not forget about the tuples, and infer array too quickly, but that would be a breaking change. we are definitely open to proposals to make these scenarios better.

  13. johnnyreilly commented on Jun 2, 2016

    @johnnyreilly
    Author

    Is the tuple the 🐓 or the 🐣?

    I'm hoping it's the 🐓. 😀

  14. makoConstruct commented on Nov 23, 2016

    @makoConstruct

    Would it be possible to have typescript think of ['str', 5] as [string,number] & (string|number)[] in these cases, instead of just one or the other?

  15. kitsonk commented on Nov 23, 2016

    @kitsonk
    Contributor

    That isn't the worst thing in the world... found one situation where it does produce surprising results:

    const foo: [string, number] & (string | number)[] = ['bar', 12];
    
    const bar = foo[0]; // string
    
    foo.push('bar');
    
    const baz = foo[1]; // number
    
    const qat = foo[2]; // string | number
    
    const qux = foo.shift(); // string | number
    
    const [a, b] = foo; // a is string, b is number but in reality it is reversed
  16. makoConstruct commented on Nov 23, 2016

    @makoConstruct

    Ohhh... I see. A tuple can't really have push or shift methods, because its type would effectively have to change every time they were called and no type system I'm aware of supports that, probably because it's more reasonable to just make tuples immutable and use slice and concat methods instead (this could even be implemented without any reallocations in languages with strong move semantics)

    So a [string,number] & (string|number)[] doesn't really make sense, conceptually, because there can't be a type that has all of the methods of both of the conjuncts. I suppose the most correct signature for ['bar', 12] would be Into<[string,number]> & Into<(string|number)[]>, it can be converted Into either of the two types, but it hasn't decided which it is yet, it is certainly not both at the same time. I don't suppose this is relevant to typescript, though, sorry.

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

    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