4

I'm working on a project where I frequently have to transform every value in an ES6 map:

const positiveMap = new Map(
  [
    ['hello', 1],
    ['world', 2]
  ]
);

const negativeMap = new Map<string, number>();
for (const key of positiveMap.keys()) {
  negativeMap.set(key, positiveMap.get(key) * -1);
}

Just wondering if there is maybe a better way of doing this? Ideally a one liner like Array.map().

Bonus points (not really), if it compiles in typescript!

NSjonas
  • 10,693
  • 9
  • 66
  • 92
  • 2
    @AluanHaddad `.entries()` returns an iterator, not an array of key/value pairs. That won't work. – Patrick Roberts Feb 01 '18 at 20:17
  • @PatrickRoberts you're right. (hits head with hand) – Aluan Haddad Feb 01 '18 at 20:18
  • for anyone that cares (about microbenchmarks) copying manually using `.forEach` is an order of magnitude faster on V8. Mutating in-place is another order of magnitude faster. Not that it will actually really matter – user120242 Jun 06 '20 at 21:50

4 Answers4

7

You could use the Array.from 2nd argument, a map-style callback:

const positiveMap = new Map([['hello', 1],['world', 2]]),
    negativeMap = new Map(Array.from(positiveMap, ([k, v]) => [k, -v]));

console.log([...negativeMap]);
trincot
  • 317,000
  • 35
  • 244
  • 286
  • It saves on creating an intermediate array before the mapping is initiated. – trincot Feb 01 '18 at 20:28
  • any idea why this throws a type error (in typescript)? https://www.typescriptlang.org/play/#src=const%20positiveMap%20%3D%20new%20Map(%5B%5B'hello'%2C%201%5D%2C%5B'world'%2C%202%5D%5D)%2C%0D%0A%20%20%20%20negativeMap%20%3D%20new%20Map(Array.from(positiveMap%2C%20(%5Bk%2C%20v%5D)%20%3D%3E%20%5Bk%2C%20-v%5D))%3B%0D%0A%0D%0Aconsole.log(%5B...negativeMap%5D)%3B – NSjonas Feb 01 '18 at 20:32
  • 1
    looks like I can just force the types using `as [string, number]` at the end of `array.from` – NSjonas Feb 01 '18 at 20:54
3

You could transform it into array using spread syntax ..., apply map() method and then again transform it to Map

const positiveMap = new Map([['hello', 1],['world', 2]]);

const negativeMap = new Map([...positiveMap].map(([k, v]) => [k, v * -1]))
console.log([...negativeMap])
Nenad Vracar
  • 118,580
  • 15
  • 151
  • 176
  • very nice! Exactly what I was looking for. I've have had issues in the past with `ts-jest` not properly transpiling the spread operator on a map... but that doesn't really relate to the questions at hand – NSjonas Feb 01 '18 at 20:25
  • 1
    @NSjonas you can use `Array.from` instead of `[...]` – uladzimir Feb 01 '18 at 20:26
  • any thoughts on how to make it compile in TS? https://www.typescriptlang.org/play/#src=const%20positiveMap%20%3D%20new%20Map(%5B%5B'hello'%2C%201%5D%2C%5B'world'%2C%202%5D%5D)%3B%0D%0A%0D%0Aconst%20negativeMap%20%3D%20new%20Map(%5B...positiveMap%5D.map((%5Bk%2C%20v%5D)%20%3D%3E%20%5Bk%2C%20v%20*%20-1%5D))%0D%0Aconsole.log(%5B...negativeMap%5D) – NSjonas Feb 01 '18 at 20:33
  • nvm, just need to force the return of array from with ` as [string, number]` – NSjonas Feb 01 '18 at 20:55
1

If you want, you can extend Map with your own class and include functionality to generically iterate it like an array:

class ArrayMap extends Map {
  map (fn, thisArg) {
    const { constructor: Map } = this;
    const map = new Map();
    
    for (const [key, value] of this.entries()) {
      map.set(key, fn.call(thisArg, value, key, this));
    }
    
    return map;
  }
  
  forEach (fn, thisArg) {
    for (const [key, value] of this.entries()) {
      fn.call(thisArg, value, key, this);
    }
  }
  
  reduce (fn, accumulator) {
    const iterator = this.entries();
    
    if (arguments.length < 2) {
      if (this.size === 0) throw new TypeError('Reduce of empty map with no initial value');
      accumulator = iterator.next().value[1];
    }
    
    for (const [key, value] of iterator) {
      accumulator = fn(accumulator, value, key, this);
    }
    
    return accumulator;
  }
  
  every (fn, thisArg) {
    for (const [key, value] of this.entries()) {
      if (!fn.call(thisArg, value, key, this)) return false;
    }
    
    return true;
  }
  
  some (fn, thisArg) {
    for (const [key, value] of this.entries()) {
      if (fn.call(thisArg, value, key, this)) return true;
    }
    
    return false;
  }
  
  // ...
}

const positiveMap = new ArrayMap(
  [
    ['hello', 1],
    ['world', 2]
  ]
);
const negativeMap = positiveMap.map(value => -value);

negativeMap.forEach((value, key) => console.log(key, value));

I threw in reduce(), every() and some() for free. Implement as many or as few of the methods you like or need.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • what are your thoughts on just extending via prototype instead (using global `types.d.ts`)? Bad idea? – NSjonas Feb 01 '18 at 20:43
  • 1
    I have little experience with TypeScript, but my general advice is if you're introducing new functionality, put it in a new class so that you don't render native classes incompatible with existing type definitions for them. – Patrick Roberts Feb 01 '18 at 20:45
0

You could have a generic typescript function based on trincot's answer

function transformMap<K, V, U>(source: Map<K, V>, func: (key: K, value: V) => U): Map<K, U> {
  return new Map(Array.from(source, (v) => [v[0], func(v[0], v[1])]));
}

and use it like this

transformMap(positiveMap, (key, value) => -value)
Sebastian
  • 503
  • 5
  • 11