214

Is there a simple way to merge ES6 Maps together (like Object.assign)? And while we're at it, what about ES6 Sets (like Array.concat)?

jameslk
  • 3,258
  • 3
  • 16
  • 17
  • 4
    This blog post has some insight. http://www.2ality.com/2015/01/es6-set-operations.html – lemieuxster Aug 14 '15 at 01:16
  • AFAIK for the _Map_ you'll need to use `for..of` because `key` can be any type – Paul S. Aug 14 '15 at 01:17
  • I tried a couple of the higher upvoted suggestions here, without success. The "union" function provided at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#implementing_basic_set_operations worked well. As a one-liner, here (note: as one-liner, I needed to add a semicolon before the "for" statement): `function union(setA, setB) { let _union = new Set(setA); for (let elem of setB) { _union.add(elem) } return _union }; set1 = new Set(['apple']); set2 = new Set(['banana']); set3 = union(set1,set2); console.log(set3) // Set [ "apple", "banana" ]` – Victoria Stuart Feb 26 '21 at 03:52

13 Answers13

316

For sets:

var merged = new Set([...set1, ...set2, ...set3])

For maps:

var merged = new Map([...map1, ...map2, ...map3])

Note that if multiple maps have the same key, the value of the merged map will be the value of the last merging map with that key.

Oriol
  • 225,583
  • 46
  • 371
  • 457
  • 4
    Documentation on [`Map`](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Map): “Constructor: `new Map([iterable])`”, “`iterable` is an Array or other iterable object whose elements are key-value pairs (2-element Arrays). Each key-value pair is added to the new Map.” — just as a reference. – Sebastian Simon Aug 14 '15 at 01:28
  • 42
    For large Sets, just be warned that this iterates the content of both Sets twice, once to create a temporary array containing the union of the two sets, then passes that temporary array to the Set constructor where it is iterated again to create the new Set. – jfriend00 Aug 14 '15 at 03:10
  • 2
    @jfriend00: see jameslk's answer below for a better method – Bergi Aug 14 '15 at 05:08
  • 1
    @Bergi why is it better? –  Aug 14 '15 at 06:34
  • 2
    @torazaburo: as jfriend00 said, Oriols solution does create unnecessary intermediate arrays. Passing an iterator to the `Map` constructor avoids their memory consumption. – Bergi Aug 14 '15 at 07:33
  • 2
    I'm bummed that ES6 Set/Map don't provide efficient merge methods. – Andy Jan 18 '17 at 22:34
  • @peterh you should try first before speaking. Example - `var a = new Set([1,2,3])`, `var b = new Set([3,4,5])`, `console.log(new Set([...a, ...b]))` prints - `Set { 1, 2, 3, 4, 5 }`. – Jyotman Singh Mar 31 '17 at 08:16
  • 2
    @JyotmanSingh You are right, I am sorry. I didn't notice the `...` :-( It is a pretty good answer! – peterh Mar 31 '17 at 08:46
  • This answer is exactly correct. Just a pointer for distracted users: this doesn't work in ES5 (or TypeScript targeted to ES5). – Guillermo Prandi Feb 04 '18 at 20:52
  • This returns empty map for me. – Audrius Meskauskas Apr 11 '18 at 08:29
  • 1
    I did not want a new Set. I wanted the original Set with the new items added in a single simple preferably built in function. The ES6 Set is woefully inadequate. No extend/update, no intersection, no union as built in operations. Fails Data Structures 101. – Samantha Atkins Feb 02 '19 at 00:44
  • 1
    Doesn't work in TypeScript. Requires each `...map` to be converted to `...Array.from(map)` – devuxer Dec 06 '19 at 22:37
  • 3
    @devuxer you need to enable `compilerOptions.downlevelIteration` in your tsconfig.json to get rid of the compiler error. See https://stackoverflow.com/questions/53441292/why-downleveliteration-is-not-on-by-default – Max Apr 18 '20 at 07:44
58

For reasons I do not understand, you cannot directly add the contents of one Set to another with a built-in operation. Operations like union, intersect, merge, etc... are pretty basic set operations, but are not built-in. Fortunately, you can construct these all yourself fairly easily.

So, to implement a merge operation (merging the contents of one Set into another or one Map into another), you can do this with a single .forEach() line:

var s = new Set([1,2,3]);
var t = new Set([4,5,6]);

t.forEach(s.add, s);
console.log(s);   // 1,2,3,4,5,6

And, for a Map, you could do this:

var s = new Map([["key1", 1], ["key2", 2]]);
var t = new Map([["key3", 3], ["key4", 4]]);

t.forEach(function(value, key) {
    s.set(key, value);
});

Or, in ES6 syntax:

t.forEach((value, key) => s.set(key, value));

FYI, if you want a simple subclass of the built-in Set object that contains a .merge() method, you can use this:

// subclass of Set that adds new methods
// Except where otherwise noted, arguments to methods
//   can be a Set, anything derived from it or an Array
// Any method that returns a new Set returns whatever class the this object is
//   allowing SetEx to be subclassed and these methods will return that subclass
//   For this to work properly, subclasses must not change behavior of SetEx methods
//
// Note that if the contructor for SetEx is passed one or more iterables, 
// it will iterate them and add the individual elements of those iterables to the Set
// If you want a Set itself added to the Set, then use the .add() method
// which remains unchanged from the original Set object.  This way you have
// a choice about how you want to add things and can do it either way.

class SetEx extends Set {
    // create a new SetEx populated with the contents of one or more iterables
    constructor(...iterables) {
        super();
        this.merge(...iterables);
    }

    // merge the items from one or more iterables into this set
    merge(...iterables) {
        for (let iterable of iterables) {
            for (let item of iterable) {
                this.add(item);
            }
        }
        return this;        
    }

    // return new SetEx object that is union of all sets passed in with the current set
    union(...sets) {
        let newSet = new this.constructor(...sets);
        newSet.merge(this);
        return newSet;
    }

    // return a new SetEx that contains the items that are in both sets
    intersect(target) {
        let newSet = new this.constructor();
        for (let item of this) {
            if (target.has(item)) {
                newSet.add(item);
            }
        }
        return newSet;        
    }

    // return a new SetEx that contains the items that are in this set, but not in target
    // target must be a Set (or something that supports .has(item) such as a Map)
    diff(target) {
        let newSet = new this.constructor();
        for (let item of this) {
            if (!target.has(item)) {
                newSet.add(item);
            }
        }
        return newSet;        
    }

    // target can be either a Set or an Array
    // return boolean which indicates if target set contains exactly same elements as this
    // target elements are iterated and checked for this.has(item)
    sameItems(target) {
        let tsize;
        if ("size" in target) {
            tsize = target.size;
        } else if ("length" in target) {
            tsize = target.length;
        } else {
            throw new TypeError("target must be an iterable like a Set with .size or .length");
        }
        if (tsize !== this.size) {
            return false;
        }
        for (let item of target) {
            if (!this.has(item)) {
                return false;
            }
        }
        return true;
    }
}

module.exports = SetEx;

This is meant to be in it's own file setex.js that you can then require() into node.js and use in place of the built-in Set.

jfriend00
  • 580,699
  • 78
  • 809
  • 825
  • 3
    I don't think `new Set(s, t)`. works. The `t` parameter is ignored. Also, it is obviously not reasonable behavior to have `add` detect the type of its parameter and if a set add the elements of the set, because then there would be no way to add a set itself to a set. –  Aug 14 '15 at 02:31
  • @torazaburo - as for the `.add()` method taking a Set, I understand your point. I just find that of far less use than being able to combine sets using `.add()` as I've never ever had a need for a Set or Sets, but I've had a need to merge sets many times. Just a matter of opinion of usefulness of one behavior vs. the other. – jfriend00 Aug 14 '15 at 02:38
  • Argh, I hate that this doesn't work for maps: `n.forEach(m.add, m)` - it does invert key/value pairs! – Bergi Aug 14 '15 at 05:09
  • @Bergi - yeah, it is odd that `Map.prototype.forEach()` and `Map.prototype.set()` have reversed arguments. Seems like an oversight by someone. It forces more code when trying to use them together. – jfriend00 Aug 15 '15 at 00:23
  • @jfriend00: OTOH, it kinda makes sense. `set` parameter order is natural for key/value pairs, `forEach` is aligned with `Array`s `forEach` method (and things like `$.each` or `_.each` that also enumerate objects). – Bergi Aug 16 '15 at 18:03
  • Add should not do magic if you pass a set, because you might want to nest a set inside a set. But it would make sense for add to take multiple parameters and add them all, then you could do `mySet.add(1,2,3,4)` or even `mySet.add(...anotherSet)`. – rjmunro May 08 '19 at 14:35
54

Here's my solution using generators:

For Maps:

let map1 = new Map(), map2 = new Map();

map1.set('a', 'foo');
map1.set('b', 'bar');
map2.set('b', 'baz');
map2.set('c', 'bazz');

let map3 = new Map(function*() { yield* map1; yield* map2; }());

console.log(Array.from(map3)); // Result: [ [ 'a', 'foo' ], [ 'b', 'baz' ], [ 'c', 'bazz' ] ]

For Sets:

let set1 = new Set(['foo', 'bar']), set2 = new Set(['bar', 'baz']);

let set3 = new Set(function*() { yield* set1; yield* set2; }());

console.log(Array.from(set3)); // Result: [ 'foo', 'bar', 'baz' ]
Bergi
  • 513,640
  • 108
  • 821
  • 1,164
jameslk
  • 3,258
  • 3
  • 16
  • 17
28

Edit:

I benchmarked my original solution against other solutions suggests here and found that it is very inefficient.

The benchmark itself is very interesting (link) It compares 3 solutions (higher is better):

  • @fregante (formerly called @bfred.it) solution, which adds values one by one (14,955 op/sec)
  • @jameslk's solution, which uses a self invoking generator (5,089 op/sec)
  • my own, which uses reduce & spread (3,434 op/sec)

As you can see, @fregante's solution is definitely the winner.

Performance + Immutability

With that in mind, here's a slightly modified version which doesn't mutates the original set and excepts a variable number of iterables to combine as arguments:

function union(...iterables) {
  const set = new Set();

  for (const iterable of iterables) {
    for (const item of iterable) {
      set.add(item);
    }
  }

  return set;
}

Usage:

const a = new Set([1, 2, 3]);
const b = new Set([1, 3, 5]);
const c = new Set([4, 5, 6]);

union(a,b,c) // {1, 2, 3, 4, 5, 6}

Original Answer

I would like to suggest another approach, using reduce and the spread operator:

Implementation

function union (sets) {
  return sets.reduce((combined, list) => {
    return new Set([...combined, ...list]);
  }, new Set());
}

Usage:

const a = new Set([1, 2, 3]);
const b = new Set([1, 3, 5]);
const c = new Set([4, 5, 6]);

union([a, b, c]) // {1, 2, 3, 4, 5, 6}

Tip:

We can also make use of the rest operator to make the interface a bit nicer:

function union (...sets) {
  return sets.reduce((combined, list) => {
    return new Set([...combined, ...list]);
  }, new Set());
}

Now, instead of passing an array of sets, we can pass an arbitrary number of arguments of sets:

union(a, b, c) // {1, 2, 3, 4, 5, 6}
Magne
  • 14,749
  • 8
  • 57
  • 77
Asaf Katz
  • 4,152
  • 4
  • 35
  • 41
  • This is horribly inefficient. – Bergi May 11 '18 at 16:08
  • 1
    Hi @Bergi, you are right. Thanks for raising my awareness (: I've tested my solutions against others suggested here and proved it for myself. Also, I've edited my answer to reflect that. Please consider removing your downvote. – Asaf Katz May 11 '18 at 18:40
  • 1
    Great, thanks for the performance comparison. Funny how the "unelegant" solution is fastest ;) Came here to look for an improvement over `forof` and `add`, which seems just very inefficient. I really wish for an `addAll(iterable)` method on Sets – Bruno Schäpper Nov 09 '18 at 08:14
  • 1
    Typescript version: `function union (...iterables: Array>): Set { const set = new Set(); iterables.forEach(iterable => { iterable.forEach(item => set.add(item)) }) return set } ` – ndp Jan 30 '19 at 00:36
  • 5
    The jsperf link near the top of your answer seems busted. Also, you refer to bfred's solution which I don't see anywhere here. – jfriend00 Aug 24 '19 at 16:36
  • Minor improvement: initialize the Set directly with the first iterable: `function union(iterable, ...iterables) { const set = new Set(iterable); ...` then the rest of the loop as before. Probably won't make *much* difference, but maybe the engine can optimize better compared to adding one by one. – nickf Nov 14 '19 at 23:19
  • 1
    @jfriend00 when I visited `bfred.it` I got a twitter account of `fregante` so maybe it is fregante's answer! – Al-Mothafar Jun 01 '20 at 15:54
  • @AsafKatz jsperf has been shut down. Have you got the performance test code somewhere else easily at hand and shareable? – Magne Dec 04 '20 at 10:31
  • https://jsbench.me can be used as an alternative to the defunct jsperf. – Magne Dec 04 '20 at 10:34
15

The approved answer is great but that creates a new set every time.

If you want to mutate an existing object instead, use a helper function.

Set

function concatSets(set, ...iterables) {
    for (const iterable of iterables) {
        for (const item of iterable) {
            set.add(item);
        }
    }
}

Usage:

const setA = new Set([1, 2, 3]);
const setB = new Set([4, 5, 6]);
const setC = new Set([7, 8, 9]);
concatSets(setA, setB, setC);
// setA will have items 1, 2, 3, 4, 5, 6, 7, 8, 9

Map

function concatMaps(map, ...iterables) {
    for (const iterable of iterables) {
        for (const item of iterable) {
            map.set(...item);
        }
    }
}

Usage:

const mapA = new Map().set('S', 1).set('P', 2);
const mapB = new Map().set('Q', 3).set('R', 4);
concatMaps(mapA, mapB);
// mapA will have items ['S', 1], ['P', 2], ['Q', 3], ['R', 4]
fregante
  • 23,010
  • 11
  • 97
  • 127
6

To merge the sets in the array Sets, you can do

var Sets = [set1, set2, set3];

var merged = new Set([].concat(...Sets.map(set => Array.from(set))));

It is slightly mysterious to me why the following, which should be equivalent, fails at least in Babel:

var merged = new Set([].concat(...Sets.map(Array.from)));
  • `Array.from` takes additional parameters, the second being a mapping function. `Array.prototype.map` passes three arguments to its callback: `(value, index, array)`, so it's effectively calling `Sets.map((set, index, array) => Array.from(set, index, array)`. Obviously, `index` is a number and not a mapping function so it fails. – nickf Nov 14 '19 at 23:23
1

Based off of Asaf Katz's answer, here's a typescript version:

export function union<T> (...iterables: Array<Set<T>>): Set<T> {
  const set = new Set<T>()
  iterables.forEach(iterable => {
    iterable.forEach(item => set.add(item))
  })
  return set
}
ndp
  • 19,946
  • 4
  • 33
  • 48
1

It does not make any sense to call new Set(...anArrayOrSet) when adding multiple elements (from either an array or another set) to an existing set.

I use this in a reduce function, and it is just plain silly. Even if you have the ...array spread operator available, you should not use it in this case, as it wastes processor, memory, and time resources.

// Add any Map or Set to another
function addAll(target, source) {
  if (target instanceof Map) {
    Array.from(source.entries()).forEach(it => target.set(it[0], it[1]))
  } else if (target instanceof Set) {
    source.forEach(it => target.add(it))
  }
}

Demo Snippet

// Add any Map or Set to another
function addAll(target, source) {
  if (target instanceof Map) {
    Array.from(source.entries()).forEach(it => target.set(it[0], it[1]))
  } else if (target instanceof Set) {
    source.forEach(it => target.add(it))
  }
}

const items1 = ['a', 'b', 'c']
const items2 = ['a', 'b', 'c', 'd']
const items3 = ['d', 'e']

let set

set = new Set(items1)
addAll(set, items2)
addAll(set, items3)
console.log('adding array to set', Array.from(set))

set = new Set(items1)
addAll(set, new Set(items2))
addAll(set, new Set(items3))
console.log('adding set to set', Array.from(set))

const map1 = [
  ['a', 1],
  ['b', 2],
  ['c', 3]
]
const map2 = [
  ['a', 1],
  ['b', 2],
  ['c', 3],
  ['d', 4]
]
const map3 = [
  ['d', 4],
  ['e', 5]
]

const map = new Map(map1)
addAll(map, new Map(map2))
addAll(map, new Map(map3))
console.log('adding map to map',
  'keys', Array.from(map.keys()),
  'values', Array.from(map.values()))
Steven Spungin
  • 17,551
  • 4
  • 57
  • 56
1

Transform the sets into arrays, flatten them and finally the constructor will uniqify.

const union = (...sets) => new Set(sets.map(s => [...s]).flat());
NicoAdrian
  • 519
  • 1
  • 4
  • 11
  • Please don't post only code as an answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually of higher quality, and are more likely to attract upvotes. – Mark Rotteveel Apr 09 '20 at 13:57
0

No, there are no builtin operations for these, but you can easily create them your own:

Map.prototype.assign = function(...maps) {
    for (const m of maps)
        for (const kv of m)
            this.add(...kv);
    return this;
};

Set.prototype.concat = function(...sets) {
    const c = this.constructor;
    let res = new (c[Symbol.species] || c)();
    for (const set of [this, ...sets])
        for (const v of set)
            res.add(v);
    return res;
};
Bergi
  • 513,640
  • 108
  • 821
  • 1,164
0

Example

const mergedMaps = (...maps) => {
    const dataMap = new Map([])

    for (const map of maps) {
        for (const [key, value] of map) {
            dataMap.set(key, value)
        }
    }

    return dataMap
}

Usage

const map = mergedMaps(new Map([[1, false]]), new Map([['foo', 'bar']]), new Map([['lat', 1241.173512]]))
Array.from(map.keys()) // [1, 'foo', 'lat']
Community
  • 1
  • 1
dimpiax
  • 9,648
  • 2
  • 51
  • 41
0

I've created a small snippet to merge any number of Sets using a function in ES6. You can change the "Set" to "Map" get it working with Maps.

const mergeSets = (...args) => {
    return new Set(args.reduce((acc, current) => {
        return [...acc, ...current];
    }, []));
};

const foo = new Set([1, 2, 3]);
const bar = new Set([1, 3, 4, 5]);

mergeSets(foo, bar); // Set(5) {1, 2, 3, 4, 5}
mergeSets(foo, bar, new Set([6])); // Set(6) {1, 2, 3, 4, 5, 6}
ddobby94
  • 273
  • 3
  • 10
-1

You can use the spread syntax to merge them together:

const map1 = {a: 1, b: 2}
const map2 = {b: 1, c: 2, a: 5}

const mergedMap = {...a, ...b}

=> {a: 5, b: 1, c: 2}
Cody Gray
  • 222,280
  • 47
  • 466
  • 543