How do I set the initial values of a typed Map in the constructor when the keys have multiple types

Viewed 1553

I want to create a Map where the key can be a number or a string. I want to initialise it with values in the constructor. Some entries have a number key and some have a string key.

But I get a typescript error. Is this a TypeScript bug, or am I doing something wrong?

type MyMap = Map<string | number, string>;

// Errors, incorrectly?
const myMapWithMixedKeys: MyMap = new Map([
  ['abc', 'String key'],
  [123, 'Number key']
]);

// All fine
myMapWithMixedKeys.set('abcd', 'String key');
myMapWithMixedKeys.set(1234, 'Number key');

const myMapStrings: MyMap = new Map([
  ['abc', 'String key'],
  ['abcd', 'String key']
]);

const myMapNumbers: MyMap = new Map([
  [123, 'Number key'],
  [1234, 'Number key']
]);

The error:

No overload matches this call.
  Overload 1 of 3, '(iterable: Iterable<readonly [string, string]>): Map<string, string>', gave the following error.
    Argument of type '([string, string] | [number, string])[]' is not assignable to parameter of type 'Iterable<readonly [string, string]>'.
      Types of property '[Symbol.iterator]' are incompatible.
        Type '() => IterableIterator<[string, string] | [number, string]>' is not assignable to type '() => Iterator<readonly [string, string], any, undefined>'.
          Type 'IterableIterator<[string, string] | [number, string]>' is not assignable to type 'Iterator<readonly [string, string], any, undefined>'.
            Types of property 'next' are incompatible.
              Type '(...args: [] | [undefined]) => IteratorResult<[string, string] | [number, string], any>' is not assignable to type '(...args: [] | [undefined]) => IteratorResult<readonly [string, string], any>'.
                Type 'IteratorResult<[string, string] | [number, string], any>' is not assignable to type 'IteratorResult<readonly [string, string], any>'.
                  Type 'IteratorYieldResult<[string, string] | [number, string]>' is not assignable to type 'IteratorResult<readonly [string, string], any>'.
                    Type 'IteratorYieldResult<[string, string] | [number, string]>' is not assignable to type 'IteratorYieldResult<readonly [string, string]>'.
                      Type '[string, string] | [number, string]' is not assignable to type 'readonly [string, string]'.
                        Type '[number, string]' is not assignable to type 'readonly [string, string]'.
                          Types of property '0' are incompatible.
                            Type 'number' is not assignable to type 'string'.
  Overload 2 of 3, '(entries?: readonly (readonly [string, string])[] | null | undefined): Map<string, string>', gave the following error.
    Argument of type '([string, string] | [number, string])[]' is not assignable to parameter of type 'readonly (readonly [string, string])[]'.
      Type '[string, string] | [number, string]' is not assignable to type 'readonly [string, string]'.
        Type '[number, string]' is not assignable to type 'readonly [string, string]'.ts(2769)
1 Answers

The problem is that TS infer the type of a new Map() by its first value. It happens even you write just let m = new Map([[1, 1], ['2', 2]]), without any typing.

(BTW: It happens also in arrays when you write something like let a = new Array(1, '2')).

To solve this you should to force TS to understand that the type of the Map is union type. I know two ways to do this:

const myMapWithMixedKeys: MyMap = new Map<string | number, string>([
  ['abc', 'String key'],
  [123, 'Number key']
]);

Or:

const myMapWithMixedKeys: MyMap = new Map([
  ['abc' as string | number, 'String key'],
  [123, 'Number key']
]);

See Playground

EDIT: I found a better solution, IMO.

Instead of:

type MyMap = Map<string | number, string>;

Write:

class MyMap extends Map<string | number, string> { }

And now everything works well. See updated Playground

Related