How to Merge Maps in TypeScript (Spread, Loops and Generics)

The quickest way to handle a TypeScript merge maps task is spreading both Maps into a new one. Each Map is an iterable of [key, value] pairs, so new Map([...a, ...b]) combines them, and the second Map wins on duplicate keys:

const merged = new Map([...austin, ...denver]);   // Map<string, number>

Every snippet in this guide went through strict-mode tsc and then Node (TypeScript 7.0.2 and Node.js v22.22.2). The outputs are pasted straight from my terminal.

Merge two Maps in TypeScript with the spread operator

Here, a retailer tracks stock by SKU in Austin and Denver warehouses. The TypeScript spread operator works here because Maps are iterable:

const austin = new Map<string, number>([
  ["MUG-12", 40],
  ["BOOK-TS", 15],
  ["MAT-YOGA", 8],
]);
const denver = new Map<string, number>([
  ["BOOK-TS", 22],
  ["LAMP-LED", 30],
]);

const merged = new Map([...austin, ...denver]);

console.log(merged);
console.log("size:", merged.size);
console.log("austin untouched:", austin.get("BOOK-TS"), "| denver size:", denver.size);

Output:

Map(4) {
  'MUG-12' => 40,
  'BOOK-TS' => 22,
  'MAT-YOGA' => 8,
  'LAMP-LED' => 30
}
size: 4
austin untouched: 15 | denver size: 2
Command Prompt output of TypeScript new Map spread merging Austin and Denver inventory Maps
BOOK-TS takes Denver’s 22 but keeps its original second position.

Neither source Map changes, and a repeated key keeps the slot where it first appeared. TypeScript infers Map<string, number> on its own.

Merge into an existing Map with for…of and set()

When you don’t want a copy, loop over the second Map and call set() to update the first in place. A for…of loop in TypeScript destructures each entry neatly:

const settings = new Map<string, string>([
  ["theme", "light"],
  ["currency", "USD"],
  ["timeZone", "America/New_York"],
]);
const userPrefs = new Map<string, string>([
  ["theme", "dark"],
  ["timeZone", "America/Chicago"],
]);

// Mutates settings in place - no new Map is created
for (const [key, value] of userPrefs) {
  settings.set(key, value);
}
console.log(settings);

// forEach passes (value, key), not (key, value)
const copy = new Map(settings);
new Map([["region", "us-east-1"]]).forEach((value, key) => copy.set(key, value));
console.log(copy.get("region"), copy.size);

Output:

Map(3) {
  'theme' => 'dark',
  'currency' => 'USD',
  'timeZone' => 'America/Chicago'
}
us-east-1 4

Watch the argument order with forEach(): the callback gets (value, key), the reverse of each entry. If you’re still building your starting Maps, see how to initialize a Map in TypeScript.

How do you handle duplicate keys when merging Maps?

Spread always lets the last value win. To add quantities, keep the first value or pick the larger one, pass a resolver:

// austin and denver Maps from the first example

function mergeWith<K, V>(a: Map<K, V>, b: Map<K, V>, resolve: (left: V, right: V, key: K) => V): Map<K, V> {
  const result = new Map(a);
  for (const [key, value] of b) {
    const existing = result.get(key);
    result.set(key, result.has(key) ? resolve(existing as V, value, key) : value);
  }
  return result;
}

console.log("sum:       ", mergeWith(austin, denver, (x, y) => x + y));
console.log("keep first:", mergeWith(austin, denver, (x) => x));
console.log("max:       ", mergeWith(austin, denver, (x, y) => Math.max(x, y)));

Output:

sum:        Map(4) {
  'MUG-12' => 40,
  'BOOK-TS' => 37,
  'MAT-YOGA' => 8,
  'LAMP-LED' => 30
}
keep first: Map(4) {
  'MUG-12' => 40,
  'BOOK-TS' => 15,
  'MAT-YOGA' => 8,
  'LAMP-LED' => 30
}
max:        Map(4) {
  'MUG-12' => 40,
  'BOOK-TS' => 22,
  'MAT-YOGA' => 8,
  'LAMP-LED' => 30
}
Command Prompt output of a TypeScript mergeWith function summing, keeping first and max values for duplicate Map keys
Same two Maps, three strategies: BOOK-TS becomes 37, 15 or 22.

The resolver also gets the key, so you can treat some keys differently. If your data starts as an array of records, create a Map from an array first, then merge.

Merge any number of Maps with a typed generic function

A rest parameter lets one helper accept any number of Maps, and the generics carry the key and value types through:

// austin and denver Maps from the first example

const chicago = new Map<string, number>([["MUG-12", 12], ["PEN-BLK", 200]]);

function mergeMaps<K, V>(...maps: Map<K, V>[]): Map<K, V> {
  const result = new Map<K, V>();
  for (const map of maps) {
    for (const [key, value] of map) result.set(key, value);
  }
  return result;
}

const all = mergeMaps(austin, denver, chicago);   // Map<string, number>
console.log(all);

Output:

Map(5) {
  'MUG-12' => 12,
  'BOOK-TS' => 22,
  'MAT-YOGA' => 8,
  'LAMP-LED' => 30,
  'PEN-BLK' => 200
}

I prefer this over long spread chains. For the arrow-function form, see generic arrow functions in TypeScript.

TypeScript errors when merged Map types don’t match

Merging a Map<string, number> with a Map<string, string> is usually a bug, and tsc catches it:

function mergeMaps<K, V>(...maps: Map<K, V>[]): Map<K, V> {
  return new Map(maps.flatMap((m) => [...m]));
}

const prices = new Map<string, number>([["MUG-12", 14.99]]);
const labels = new Map<string, string>([["MUG-12", "Coffee mug"]]);

const typed = mergeMaps(prices, labels);

const spread = new Map([...prices, ...labels]);

const twoArgs = new Map(prices, labels);

Output:

merge_errors.ts(8,33): error TS2345: Argument of type 'Map<string, string>' is not assignable to parameter of type 'Map<string, number>'.
  Type 'string' is not assignable to type 'number'.
merge_errors.ts(10,24): error TS2769: No overload matches this call.
  The last overload gave the following error.
    Argument of type '([string, string] | [string, number])[]' is not assignable to parameter of type 'readonly (readonly [string, string])[]'.
      Type '[string, string] | [string, number]' is not assignable to type 'readonly [string, string]'.
        Type '[string, number]' is not assignable to type 'readonly [string, string]'.
          Type at position 1 in source is not compatible with type at position 1 in target.
            Type 'number' is not assignable to type 'string'.
merge_errors.ts(12,33): error TS2554: Expected 0-1 arguments, but got 2.
Command Prompt output of tsc errors TS2345, TS2769 and TS2554 when merging TypeScript Maps
TS2345 from the generic helper, TS2769 from the spread and TS2554 from passing two Maps to the constructor.

I expected the spread to widen to string | number, but the constructor infers its types from the first entry and rejects the rest. And new Map(a, b) isn’t a merge; the constructor takes one iterable.

Merge Maps that hold objects without sharing references

Spread copies entries, not the objects inside them. Edit a value in the merged Map and the original changes too. structuredClone() copies Maps and their nested objects:

interface Store { city: string; salesUSD: number }

const q1 = new Map<string, Store>([["TX-01", { city: "Austin", salesUSD: 48250.5 }]]);
const q2 = new Map<string, Store>([["CO-02", { city: "Denver", salesUSD: 39120 }]]);

const shallow = new Map([...q1, ...q2]);
shallow.get("TX-01")!.salesUSD = 0;
console.log("q1 after editing shallow:", q1.get("TX-01"));

q1.get("TX-01")!.salesUSD = 48250.5;
const deep = new Map([...structuredClone(q1), ...structuredClone(q2)]);
deep.get("TX-01")!.salesUSD = 0;
console.log("q1 after editing deep:   ", q1.get("TX-01"));

Output:

q1 after editing shallow: { city: 'Austin', salesUSD: 0 }
q1 after editing deep:    { city: 'Austin', salesUSD: 48250.5 }

Clone only when you’ll mutate values. The deep clone guide compares other options, and merging objects in TypeScript helps when the resolver should combine two objects.

Merge a Map with a Record or plain object

Object.entries() turns a plain object into pairs you can spread next to a Map. Here, updated state sales tax rates override the old ones:

const salesTax = new Map<string, number>([["TX", 0.0625], ["CO", 0.029]]);
const updates: Record<string, number> = { CO: 0.03, IL: 0.0625 };

const rates = new Map([...salesTax, ...Object.entries(updates)]);
console.log(rates);

const asObject = Object.fromEntries(rates);
console.log(asObject);

console.log("IL tax on $120.00:", "$" + (120 * rates.get("IL")!).toFixed(2));

Output:

Map(3) { 'TX' => 0.0625, 'CO' => 0.03, 'IL' => 0.0625 }
{ TX: 0.0625, CO: 0.03, IL: 0.0625 }
IL tax on $120.00: $7.50
Command Prompt output of a TypeScript Map merged with a Record using Object.entries and converted back with Object.fromEntries
Colorado’s rate updates to 3%, Illinois is added, and the object form matches.

Object.fromEntries() goes the other way when an API expects an object. The trade-offs between the two shapes are in Record vs Map in TypeScript, and simple pairs are covered in TypeScript key-value pairs.

Common Map merge mistakes

Object spread doesn’t read Map entries; it copies own enumerable properties, and a Map has none. JSON.stringify() has the same blind spot:

// austin and denver Maps from the first example

const asObject = { ...austin, ...denver };
console.log("object spread:", asObject);

const correct = new Map([...austin, ...denver]);
console.log("JSON.stringify:", JSON.stringify(correct));
console.log("as entries:    ", JSON.stringify([...correct]));

Output:

object spread: {}
JSON.stringify: {}
as entries:     [["MUG-12",40],["BOOK-TS",22],["MAT-YOGA",8],["LAMP-LED",30]]

Both print empty objects without any error. Serialize [...map] instead, or follow converting a Map to an array in TypeScript.

Pick one of these next if you’re working with collections:

Frequently asked questions

How do I merge two Maps in TypeScript?

Use new Map([...a, ...b]). It returns a new Map, leaves both sources unchanged, and values from b win on duplicate keys.

Which value wins when both Maps have the same key?

The one set last. With spread, that’s the later Map; the key keeps its original insertion position.

How do I sum values instead of overwriting them?

Use a resolver, as in the mergeWith helper above, or call set(key, (a.get(key) ?? 0) + value) in a loop.

Can I merge Maps with different value types?

Not without saying so. Both spread (TS2769) and a generic helper (TS2345) reject it; annotate new Map<string, string | number>(...) if you really want a union.

Does merging Maps copy nested objects?

No. It’s shallow. Wrap the sources in structuredClone() before merging if you plan to mutate the values.

Where is the Map constructor documented?

See the Map() constructor on MDN.