289

Is there any way to map/reduce/filter/etc a Set in JavaScript or will I have to write my own?

Here's some sensible Set.prototype extensions

Set.prototype.map = function map(f) {
  var newSet = new Set();
  for (var v of this.values()) newSet.add(f(v));
  return newSet;
};

Set.prototype.reduce = function(f,initial) {
  var result = initial;
  for (var v of this) result = f(result, v);
  return result;
};

Set.prototype.filter = function filter(f) {
  var newSet = new Set();
  for (var v of this) if(f(v)) newSet.add(v);
  return newSet;
};

Set.prototype.every = function every(f) {
  for (var v of this) if (!f(v)) return false;
  return true;
};

Set.prototype.some = function some(f) {
  for (var v of this) if (f(v)) return true;
  return false;
};

Let's take a little set

let s = new Set([1,2,3,4]);

And some stupid little functions

const times10 = x => x * 10;
const add = (x,y) => x + y;
const even = x => x % 2 === 0;

And see how they work

s.map(times10);    //=> Set {10,20,30,40}
s.reduce(add, 0);  //=> 10
s.filter(even);    //=> Set {2,4}
s.every(even);     //=> false
s.some(even);      //=> true

Isn't that nice ? Yeah, I think so too. Compare that to the ugly iterator usage

// puke
let newSet = new Set();
for (let v in s) {
  newSet.add(times10(v));
}

And

// barf
let sum = 0;
for (let v in s) {
  sum = sum + v;
}

Is there any better way to accomplish map and reduce using a Set in JavaScript?

12
  • The problem with map-reduce-ing a Set is that Sets aren't Functors. Commented Oct 20, 2015 at 10:53
  • 5
    Well, consider var s = new Set([1,2,3,4]); s.map((a) => 42);. It changes the number of elements, which map typically isn't supposed to do. Even worse if you're only comparing parts of the kept objects, because then technically it's unspecified which one you'll get. Commented Oct 20, 2015 at 10:55
  • I had considered that, but I'm not sure I (personally) would consider that invalid. OK so at least forEach exists for that scenario, but why no reduce then ? Commented Oct 20, 2015 at 10:58
  • I'd say that is an oversight. It's perfectly fine to fold (reduce) a Set. Commented Oct 20, 2015 at 10:59
  • 4
    Some related reading: esdiscuss.org/topic/set-some-every-reduce-filter-map-methods Commented Oct 20, 2015 at 11:01

5 Answers 5

264

A short-hand way to do it is to convert it to an array via the ES6 spread operator.

Then all the array functions are available to you.

const mySet = new Set([1,2,3,4]);
[...mySet].reduce(...);
Sign up to request clarification or add additional context in comments.

7 Comments

Because the functions are not available for Set! This is a complete, guided and understood workaround that is not as yet present in this topic. The fact it 'takes longer' is a sad price to pay for a workaround until Set implements these features!
What's the difference between this and Array.from
For me at least, the difference between this and Array.from is that Array.from works with TypeScript. Using [...mySet] gives the error: TS2461: Type 'Set<number>' is not an array type.
For spread vs Array.from(), see stackoverflow.com/a/40549565/5516454 Basically, both are usable here. Array.from() can additionally do array-like objects which do not implement the @@iterator method.
In V8 at least, another difference is that [...mySet] will fail with stack overflow for large Sets. Array.from() does not use stack memory per element, so it is less risky when the number of elements may be large.
|
30

To sum up the discussion from comments: while there are no technical reasons for set to not have reduce, it's not currently provided and we can only hope it changes in ES7.

As for map, calling it alone could violate the Set constraint, so its presence here might be debatable.

Consider mapping with a function (a) => 42 - it will change the set's size to 1, and this might or might not be what you wanted.

If you're ok with violating that because e.g. you're going to fold anyway, you can apply the map part on every element just before passing them to reduce, thus accepting that the intermediate collection (which isn't a Set at this point) that's going to be reduced might have duplicated elements. This is essentially equivalent to converting to Array to do processing.

4 Comments

This is mostly good, except (using the code above), s.map(a => 42) will result in Set { 42 } so the mapped result will be a different length but there will not be "duplicated" elements. Maybe update the wording and I'll accept this answer.
@naomik Oh derp I was just finishing my first coffee when writing that. On the second look, the intermediate collection passed to reduce might have immediate elements if you accept it's not a set - that's I meant.
Oh I get it - map has to map to the same type, hence possible collisions in the destination set. When I found this question I was thinking map would map to an array from a set. (as if you did set.toArray().map()`
In Scala and Haskell, sets support a map operation - it can reduce the number of elements in the set.
18

The cause of the lack of map/reduce/filter on Map/Set collections seem to be mainly conceptual concerns. Should each collection type in Javascript actually specify its own iterative methods only to allow this

const mySet = new Set([1,2,3]);
const myMap = new Map([[1,1],[2,2],[3,3]]);

mySet.map(x => x + 1);
myMap.map(([k, x]) => [k, x + 1]);

instead of

new Set(Array.from(mySet.values(), x => x + 1));
new Map(Array.from(myMap.entries(), ([k, x]) => [k, x + 1]));

An alternative were to specify map/reduce/filter as part of the iterable/iterator protocol, since entries/values/keys return Iterators. It is conceivable though that not every iterable is also "mappable". Another alternative were to specify a separate "collection protocol" for this very purpose.

However, I do not know the current discussion on this topic at ES.

3 Comments

Should each collection type in Javascript actually specify its own iterative methods only to allow this? Yes. All that Array.from with new Set is a workaround and is much less readable than myArray.filter(isBiggerThan6Predicate); Now I have to write my own filterSet function, so I can write clean code: filterSet(setWithNumbers, biggerThan6Predicate);
How is it about performance? I could imagine that new Set(Array.from(mySet...)) has a lot of overhead compared to an own iterative method.
That's one of my worries. I'm using a map to store objects with ID, but for each filtering of .values() it seems as if I have to create an Array copy first to then filter and create a new result array... which seems like a lot of overhead? I could use a for each loop to avoid the intermediate copy for filtering but that is so verbose.
7

As of ECMAScript 2025 similar methods now exist as iterator helper methods. That means you can use those in a functional expression while still avoiding the creation of intermediate arrays.

new Set(s.values().map(times10));  //=> Set {10,20,30,40}
s.values().reduce(add, 0);         //=> 10
new Set(s.values().filter(even));  //=> Set {2,4}
s.values().every(even);            //=> false
s.values().some(even);             //=> true

5 Comments

Which type is s in your code? Because I tried it with a set and it did not work (node.js) let a = new Set([10,11,12]) function times10(item){ return item * 10; } This a.values().map(times10) got me a Uncaught TypeError: a.values(...).map is not a function
Yes, s is that set. Make sure you have ECMAScript 2025 support. If you are on Node.js, you need version 22.11 or higher.
Thanks! Now on an unrelated note, doe these execute lazily, like Python iterators?
Yes, they are lazy, like Python iterators and Java Streams. [1,2].values().map(x => (console.log(x), x)).forEach(x => console.log(x)) will output 1 1 2 2.
This is now the correct answer.
0

Thought this was worth mentioning in terms of speed, if you are deciding between the Set Methods forEach() which invokes a callback for each element, or values() which returns an iterator with all the values in a Set.

As an example lets filer out the even numbers from a Set:

const generateSet = (n, m) => {
  // Generate list of length n with random numbers between 0 and m
  let arr = Array.from({ length: n }, () =>
    Math.floor(Math.random() * m)
  );
  // Convert to Set
  var set = new Set(arr);
  return set;
};

Our two filter functions:

const filterValues = (set) => {
  // Using Iterator
  const it = set.values();
  let result = it.next();
  while (!result.done) {
    if (result.value % 2 === 0) {
      set.delete(result.value);
    }
    result = it.next();
  }
};

const filterForEach = (set) => {
  // invokes a callback
  set.forEach((item) => {
    if (item % 2 === 0) {
      set.delete(item);
    }
  });
};

For timing we use Timing these on random array including numbers from range [0, 10,000,000] containing 5,000,000 items:

let setValues = generateSet(5000000, 10000000);
console.time("Filter with values()");
filterValues(setValues);
console.timeEnd("Filter with values()");

let setForEach = generateSet(5000000, 10000000);
console.time("Filter with forEach()");
filterForEach(setForEach);
console.timeEnd("Filter with forEach()");

Results are:

Filter with values(): 399.456ms
Filter with forEach(): 374.698ms

Or you could just stick with the Array method:

const arrMethod = (set) => {
  // Using Array method
  const filter = [...set].filter((item) => item % 2 === 0);
  return filter;
};

let setArray = generateSet(5000000, 10000000);
console.time("Filter with array");
filterForEach(setArray);
console.timeEnd("Filter with array");

It seems consistently faster...

Filter with values(): 356.486ms
Filter with forEach(): 386.825ms
Filter with array: 342.358ms

1 Comment

Note that the array method returns an array not a set.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.