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.
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.
Thanks for commenting @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 :smile:).
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)...
TypeScript should be able to infer a tuple type in the Map constructor. Might #8407 be related?
Oh that's interesting @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...
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.
So you think the PR will resolve this? Or is that only in the case where the target is ES5? (I'm targeting ES6)
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])
);
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.
Cheers for the alternate syntax @ivogabe.
As noted by @ivogabe and @kitsonk tuple types are never inferred without a contextual type.
@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).
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.
Is the tuple the 馃悡 or the 馃悾?
I'm hoping it's the 馃悡. 馃榾
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?
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
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.
Most helpful comment
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: