172

How would you do this? Instinctively, I want to do:

var myMap = new Map([["thing1", 1], ["thing2", 2], ["thing3", 3]]);

// wishful, ignorant thinking
var newMap = myMap.map((key, value) => value + 1); // Map { 'thing1' => 2, 'thing2' => 3, 'thing3' => 4 }

I've haven't gleaned much from the documentation on the new iteration protocol.

I am aware of wu.js, but I'm running a Babel project and don't want to include Traceur, which it seems like it currently depends on.

I also am a bit clueless as to how to extract how fitzgen/wu.js did it into my own project.

Would love a clear, concise explanation of what I'm missing here. Thanks!


Docs for ES6 Map, FYI

David Braun
  • 5,573
  • 3
  • 36
  • 42
neezer
  • 19,720
  • 33
  • 121
  • 220
  • Are you able to use `Array.from`? – Ry- Jun 27 '15 at 02:44
  • @minitech Possibly, with a [polyfill](http://babeljs.io/docs/advanced/caveats/)... is there not a way to do this without it? – neezer Jun 27 '15 at 02:54
  • Well, you could write your own `map` function for use on iterables, using `for of`. – Ry- Jun 27 '15 at 02:57
  • Super nitpick, but if you were really going to _map_ over a Map, you'd get back a Map at the end. Otherwise you're just first converting a Map into an Array and mapping over an Array, not .map()ing a Map. You could easily map over a Map by using an ISO: dimap(x=>[...x], x=> new Map(x)); – Dtipson Feb 24 '16 at 22:42
  • @Ry well, we probably can write our own programming language, but why..? It's quite simple thing and exists in most programming languages for many decades. – ruX Oct 27 '18 at 11:24
  • @ruX: The difference is that “write your own `map`” is a practical option (and it’s what people did for arrays before `Array.prototype.map` existed) and “write your own programming language” is much less practical. JavaScript doesn’t have a `map` from iterable to iterable, sorry. What do you want me to say? – Ry- Oct 27 '18 at 19:11

15 Answers15

124

So .map itself only offers one value you care about... That said, there are a few ways of tackling this:

// instantiation
const myMap = new Map([
  [ "A", 1 ],
  [ "B", 2 ]
]);

// what's built into Map for you
myMap.forEach( (val, key) => console.log(key, val) ); // "A 1", "B 2"

// what Array can do for you
Array.from( myMap ).map(([key, value]) => ({ key, value })); // [{key:"A", value: 1}, ... ]

// less awesome iteration
let entries = myMap.entries( );
for (let entry of entries) {
  console.log(entry);
}

Note, I'm using a lot of new stuff in that second example... ...Array.from takes any iterable (any time you'd use [].slice.call( ), plus Sets and Maps) and turns it into an array... ...Maps, when coerced into an array, turn into an array of arrays, where el[0] === key && el[1] === value; (basically, in the same format that I prefilled my example Map with, above).

I'm using destructuring of the array in the argument position of the lambda, to assign those array spots to values, before returning an object for each el.

If you're using Babel, in production, you're going to need to use Babel's browser polyfill (which includes "core-js" and Facebook's "regenerator").
I'm quite certain it contains Array.from.

Norguard
  • 26,167
  • 5
  • 41
  • 49
  • Yeah, just noticing that it looks like I'll need `Array.from` in order to do this. Your first example doesn't return an array, btw. – neezer Jun 27 '15 at 03:06
  • @neezer no, it doesn't. `.forEach` returns `undefined` flat out, all the time, as the expected use of `forEach` is a simple side-effect based iteration (same as it has been since ES5). In order to use that `forEach`, you'd want to build an array and populate it by hand, either through said `forEach` or through the iterator returned by `.entries()` or `.keys( )` or `.values( )`. `Array.from` isn't doing anything mind-bendingly unique, it's just hiding that particular ugliness of doing said traversal. And yes, check that by including `babel-core/browser.js` (or whatever it is) you get `.from`... – Norguard Jun 27 '15 at 03:08
  • 7
    See my answer, `Array.from` takes a map function as a param, you don't need create a temporary array just to map over it and discard it. – loganfsmyth Jun 27 '15 at 03:30
  • Can you explain why the object needs a parentheses wrapped around it? I'm still a little new to ES6 and having trouble understanding that part. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#Returning_object_literals says that it gets interpreted as a label and im not sure what that means – dtc Jan 20 '17 at 18:49
  • 1
    @dtc yeah, when you write an object you write `const obj = { x: 1 };`, when you write a block of code, you write `if (...) { /* block */ }`, when you write an arrow function you write `() => { return 1; }`, so how does it know when it's a function body, versus the braces of an object? And the answer is parentheses. Otherwise, it expects that the name is a label for a code-block a rarely used feature, but you can label a `switch` or a loop, so that you can `break;` out of them by name. So if it's missing, you have a function body, with a label, and the statement `1`, based on the MDN example – Norguard Jan 20 '17 at 21:08
  • `.forEach()` is not `.map()`. Why do the Javascript guys keep missing off basic APIs like this? Next you'll tell me you can't `.map()` an `Iterator`! – Timmmm Feb 12 '23 at 17:27
  • I will tell you that. Because you can't map a JavaScript iterator. Iterators can't be treated as functors, nor can they have catamorphisms applied to them in any standard fashion. Despite this answer being 8 years old, you still need to convert to an array or some userland solution which passes as a functor, and then convert back. – Norguard Feb 15 '23 at 22:18
78

Just use Array.from(iterable, [mapFn]).

var myMap = new Map([["thing1", 1], ["thing2", 2], ["thing3", 3]]);

var newEntries = Array.from(myMap, ([key, value]) => [key, value + 1]);
var newMap = new Map(newEntries);
loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
  • 1
    Thanks for this answer, I wish it were higher up. I use the pattern of spreading a Map into a temporary array all the time, blithely unaware `Array.from()` took a mapping function as an optional second parameter. – Andru Nov 06 '20 at 10:14
  • This is so nice. And it plays nice with Flow too, when using `Map.values()`, unlike Array.values() which _still_ does not :facepalm: – ericsoco Dec 05 '20 at 00:10
  • I suppose this answer must be the chosen by the OP. As this is the only and correct way to do so. – Bender Jun 09 '21 at 10:12
  • 1
    When running this in typescript playground it required explicitly specifying the type of `newEntries` variable otherwise it inferred the incorrect type. `var newEntries: Array<[string, number]> = Array.from(myMap, ([key, value]) => [key, value + 1]);` – Omar Rodriguez Mar 21 '22 at 18:05
  • 1
    @OmarRodriguez with typescript@4.6.2, this doesn't seem to be necessary any longer. – wegry May 23 '22 at 13:04
  • @wegry On typescript 4.8.3, I still have a typing issue without explicitly setting the type of newEntries. – Sébastien BATEZAT Sep 25 '22 at 09:29
42

You should just use Spread operator:

var myMap = new Map([["thing1", 1], ["thing2", 2], ["thing3", 3]]);

var newArr = [...myMap].map(value => value[1] + 1);
console.log(newArr); //[2, 3, 4]

var newArr2 = [for(value of myMap) value = value[1] + 1];
console.log(newArr2); //[2, 3, 4]
Cœur
  • 37,241
  • 25
  • 195
  • 267
  • Looks like it still requires `Array.from`, FYI. – neezer Jun 27 '15 at 03:13
  • 1
    @neezer you absolutely, positively *must* use Babel's polyfill, in order to use Babel-transpiled code on your page, in production. There's no way around it, unless you implement all of those methods (including Facebook's Regenerator) by yourself, by hand... – Norguard Jun 27 '15 at 03:15
  • @Norguard Got it -- just a configuration change I'll need to make is all I meant. More of an aside, really. :) – neezer Jun 27 '15 at 03:21
  • @neezer Indeed. It was a choice made with Babel quite some time ago (I think it was still a ways off from being renamed from 6to5). Basically, the things that could be polyfilled were put in the browser polyfill, and any inline helper-code was written that way, as few times as possible, to cut down on redundant code, and referenced the polyfilled features for the same reason. – Norguard Jun 27 '15 at 03:24
  • 7
    Just to note, `[...myMap].map(mapFn)` is going to create a a temporary array and then discard it. `Array.from` takes a `mapFn` as its second argument, just use that. – loganfsmyth Jun 27 '15 at 03:34
  • @neezer `[...myMap].map(value => value[1] + 1)` try it. – Walter Chapilliquen - wZVanG Jun 27 '15 at 03:42
  • 3
    @wZVanG Why not `Array.from(myMap.values(), val => val + 1);`? – loganfsmyth Jun 27 '15 at 03:44
7

You can use this function:

function mapMap(map, fn) {
  return new Map(Array.from(map, ([key, value]) => [key, fn(value, key, map)]));
}

usage:

var map1 = new Map([["A", 2], ["B", 3], ["C", 4]]);

var map2 = mapMap(map1, v => v * v);

console.log(map1, map2);
/*
Map { A → 2, B → 3, C → 4 }
Map { A → 4, B → 9, C → 16 }
*/
Yukulélé
  • 15,644
  • 10
  • 70
  • 94
5

In typescript, in case somebody would need it :

export {}

declare global {
    interface Map<K, V> {
        map<T>(predicate: (key: K, value: V) => T): Map<V, T>
    }
}

Map.prototype.map = function<K, V, T>(predicate: (value: V, key: K) => T): Map<K, T> {
    let map: Map<K, T> = new Map()

    this.forEach((value: V, key: K) => {
        map.set(key, predicate(value, key))
    })
    return map
}
Jerem Lachkar
  • 1,008
  • 11
  • 24
  • This works, and is my preferred approach, but now none of the other Map functions, such as `get()` are available on `Map`. Any idea what I can do about this? I have described my problem in this post: https://stackoverflow.com/questions/74628273/using-map-over-a-mapk-v-in-typescript – serlingpa Nov 30 '22 at 13:05
  • 1
    Actually scrap that. It was my client code that was broken, thus yielding `undefined` for the various methods, such as `.set()`. Fixed now. – serlingpa Nov 30 '22 at 15:42
3

Using Array.from I wrote a Typescript function that maps the values:

function mapKeys<T, V, U>(m: Map<T, V>, fn: (this: void, v: V) => U): Map<T, U> {
  function transformPair([k, v]: [T, V]): [T, U] {
    return [k, fn(v)]
  }
  return new Map(Array.from(m.entries(), transformPair));
}

const m = new Map([[1, 2], [3, 4]]);
console.log(mapKeys(m, i => i + 1));
// Map { 1 => 3, 3 => 5 }
saul.shanabrook
  • 3,068
  • 3
  • 31
  • 49
3

Actually you can still have a Map with the original keys after converting to array with Array.from. That's possible by returning an array, where the first item is the key, and the second is the transformed value.

const originalMap = new Map([
  ["thing1", 1], ["thing2", 2], ["thing3", 3]
]);

const arrayMap = Array.from(originalMap, ([key, value]) => {
    return [key, value + 1]; // return an array
});

const alteredMap = new Map(arrayMap);

console.log(originalMap); // Map { 'thing1' => 1, 'thing2' => 2, 'thing3' => 3 }
console.log(alteredMap);  // Map { 'thing1' => 2, 'thing2' => 3, 'thing3' => 4 }

If you don't return that key as the first array item, you loose your Map keys.

mayid
  • 1,644
  • 15
  • 23
2

You can map() arrays, but there is no such operation for Maps. The solution from Dr. Axel Rauschmayer:

  • Convert the map into an array of [key,value] pairs.
  • Map or filter the array.
  • Convert the result back to a map.

Example:

let map0 = new Map([
  [1, "a"],
  [2, "b"],
  [3, "c"]
]);

const map1 = new Map(
  [...map0]
  .map(([k, v]) => [k * 2, '_' + v])
);

resulted in

{2 => '_a', 4 => '_b', 6 => '_c'}
Roman
  • 19,236
  • 15
  • 93
  • 97
1

I prefer to extend the map

export class UtilMap extends Map {  
  constructor(...args) { super(args); }  
  public map(supplier) {
      const mapped = new UtilMap();
      this.forEach(((value, key) => mapped.set(key, supplier(value, key)) ));
      return mapped;
  };
}
1

You can use myMap.forEach, and in each loop, using map.set to change value.

myMap = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

for (var [key, value] of myMap.entries()) {
  console.log(key + ' = ' + value);
}


myMap.forEach((value, key, map) => {
  map.set(key, value+1)
})

for (var [key, value] of myMap.entries()) {
  console.log(key + ' = ' + value);
}
Hunter Liu
  • 19
  • 4
0

const mapMap = (callback, map) => new Map(Array.from(map).map(callback))

var myMap = new Map([["thing1", 1], ["thing2", 2], ["thing3", 3]]);

var newMap = mapMap((pair) => [pair[0], pair[1] + 1], myMap); // Map { 'thing1' => 2, 'thing2' => 3, 'thing3' => 4 }
David Braun
  • 5,573
  • 3
  • 36
  • 42
0

If you don't want to convert the entire Map into an array beforehand, and/or destructure key-value arrays, you can use this silly function:

/**
 * Map over an ES6 Map.
 *
 * @param {Map} map
 * @param {Function} cb Callback. Receives two arguments: key, value.
 * @returns {Array}
 */
function mapMap(map, cb) {
  let out = new Array(map.size);
  let i = 0;
  map.forEach((val, key) => {
    out[i++] = cb(key, val);
  });
  return out;
}

let map = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

console.log(
  mapMap(map, (k, v) => `${k}-${v}`).join(', ')
); // a-1, b-2, c-3
mpen
  • 272,448
  • 266
  • 850
  • 1,236
0
Map.prototype.map = function(callback) {
  const output = new Map()
  this.forEach((element, key)=>{
    output.set(key, callback(element, key))
  })
  return output
}

const myMap = new Map([["thing1", 1], ["thing2", 2], ["thing3", 3]])
// no longer wishful thinking
const newMap = myMap.map((value, key) => value + 1)
console.info(myMap, newMap)

Depends on your religious fervor in avoiding editing prototypes, but, I find this lets me keep it intuitive.

Commi
  • 101
  • 1
  • 2
0

Here is a Typescript variant which builds upon some of the existing ideas and attempts to be flexible in the following ways:

  • Both key and value types of output map can differ from input
  • The mapper function can access the input map itself, if it likes
function mapMap<TKI, TVI, TKO, TVO>(map: Map<TKI, TVI>,
        f: (k: TKI, v: TVI, m: Map<TKI, TVI>) => [TKO, TVO]) : Map<TKO, TVO>{
    return new Map([...map].map(p => f(p[0], p[1], map)))
}

Note: this code uses the spread operator, like some of the existing ideas, and so needs a target of of 'es2015' or higher according to my VS Code Intellisense.

Colm Bhandal
  • 3,343
  • 2
  • 18
  • 29
-1

Maybe this way:

const m = new Map([["a", 1], ["b", 2], ["c", 3]]);
m.map((k, v) => [k, v * 2]); // Map { 'a' => 2, 'b' => 4, 'c' => 6 }

You would only need to monkey patch Map before:

Map.prototype.map = function(func){
    return new Map(Array.from(this, ([k, v]) => func(k, v)));
}

We could have wrote a simpler form of this patch:

Map.prototype.map = function(func){
    return new Map(Array.from(this, func));
}

But we would have forced us to then write m.map(([k, v]) => [k, v * 2]); which seems a bit more painful and ugly to me.

Mapping values only

We could also map values only, but I wouldn't advice going for that solution as it is too specific. Nevertheless it can be done and we would have the following API:

const m = new Map([["a", 1], ["b", 2], ["c", 3]]);
m.map(v => v * 2); // Map { 'a' => 2, 'b' => 4, 'c' => 6 }

Just like before patching this way:

Map.prototype.map = function(func){
    return new Map(Array.from(this, ([k, v]) => [k, func(v)]));
}

Maybe you can have both, naming the second mapValues to make it clear that you are not actually mapping the object as it would probably be expected.

cglacet
  • 8,873
  • 4
  • 45
  • 60