'No overload matches this call' when trying to return a Map

Viewed 1165

When trying to create a Map I receive a strange error that I don't quite understand:

TS2769: No overload matches this call.
  Overload 1 of 4, '(iterable: Iterable<readonly [string, number]>): Map<string, number>', gave the following error.
    Argument of type '(string | number)[][]' is not assignable to parameter of type 'Iterable<readonly [string, number]>'.
      The types returned by '[Symbol.iterator]().next(...)' are incompatible between these types.
        Type 'IteratorResult<(string | number)[], any>' is not assignable to type 'IteratorResult<readonly [string, number], any>'.

In my code, I am taking in an array and trying to construct a map object of it

function createMapOfHeadersToIndex(headers: string[]): Map<string, number> {
  const headersWithIndex =
      headers.map(header => [header, Number(headers.indexOf(header))]);
  return new Map(headersWithIndex);
}
1 Answers

Since you didn't annotate the type of headersWithIndex, the compiler inferred its type for you, based on heuristic rules about what people tend to do with arrays:

const headersWithIndex =
  headers.map(header => [header, Number(headers.indexOf(header))]);
// const headersWithIndex: (string | number)[][]

That type, (string | number)[][] means "an array of arrays of string or number elements". That's not an incorrect description of headersWithIndex, and in other circumstances it would be a reasonable guess about what kinds of things you're likely to do with it:

headersWithIndex.push([1, "two", 3, "four"]); // okay

Unfortunately, that type is not specific enough to be accepted by the Map constructor, which expects an iterable whose elements are explicitly a 2-tuple of a key followed by a value. And so the construct call fails, because the compiler has forgotten anything about the particular ordering and length of the arrays inside the headersWithIndex elements.


The most straightforward way to fix this is to use a const assertion when you are creating the array literal containing the key and value:

const headersWithIndex =
  headers.map(header => [header, Number(headers.indexOf(header))] as const);
// const headersWithIndex: (readonly [string, number])[]
return new Map(headersWithIndex); //okay

That as const gives the compiler a hint that you'd like it to infer something like the most specific type it can; it's almost like saying "I'm not going to change the contents of this thing, so please pay attention to exactly where everything is". And you'll see that now headersWithIndex is inferred as type (readonly [string, number])[]; that is, an array of readonly tuples consisting of one string followed by one number.

And now the Map constructor is happy, since it knows for sure that each element of headersWithIndex is a pair of a string key and a number value, resulting in a Map<string, number>.

Playground link to code

Related