Improvement on the Map standard library to better serve its users.

Problem:
I have a Map instance in which it's keys have Map instance and values too, imagine a nested Map situation. I call this Nested Map. See below:

const m = new Map([["a", "aa"], ["m", new Map([["b", "bb"], ["mm", new Map()]])]]);

I wanted to serialize this whole thing, and store in local storage, of course I have to spread it into an array mapping first as we know it won't work as expected for Maps.

Applying spread operator or calling the entries method on a Map that doesn't have another Map instance as its key value will work as expected. But having a nested situation like that, only works for the outer Map instance. Even the entries method will create iterator just for the main instance you're calling it on, not for the inner ones.

My way around this is, using a replacer function that spreads values which are map instances into an array (See below), before storing in local storage.

JSON.stringify(obj, (key, value) => {
	if (({}).toString.call(value) === '[Object Map]') return [...value];
	return value;
});

Proposal:
Let's have a way in which Map instances... maybe have a method that create iterators not just parsing main Map instance it is being called on, but also the key values which are Map instances also.

Also applying spread operator on it will spread from the main Map instance to the deepest key value Map instance, the inner key value Map instances will spread in an array too, so that, at the end we have an Array mapping in which the values for keys which previously have Map instances as values are now array mappings too, to the same level of initially nested Map instances. Imagine a deep flat situation, but not the array kind.

More interesting: A way to reverse the situation, so that we are back to our formal nested Map instances.
My way around is: getting my nested array from the local storage, for...of looping through the whole nested array, setting keys and values all over again in the map, and if the value part of the destructure is an array, I set a Map instance as value for the current key, and setting the keys and values for that nested one too, in a nested for...of loop.
That's tedious for anyone, even the V8 engine.
Here:

for(const [key, value] of nestedArray) {
	if (Array.isArray(value)) {
		m.set(key, new Map());
		for (const [key2, value2] of value) {
			m.get(key).set(key2, value2);
		} 
	} else {
		m.set(key, value);
	} 
}
/*If **value2** is an array, this keeps growing...
There might be way to refactor this, maybe
with the **reduce method**, but it's still an hack
that might cause issues if you don't know what
you're doing.*/ 

This is what I do whenever I want a solution to problems like this.

It'd save precious time, if I can just make my nested Map, into array mappings, serialize it, store locally, parse it, and convert back to nested Map.

An idea :bulb: while typing:
This can also allow, having an Array Mapping in which the value part (not all, of course) are array mappings too(instead of any other value or a map instance), go in the Map constructor, and having a dynamically created Map instance for each Array mapping at the value part in the Array Mapping passed to the Map constructor. See below:

const m = new Map([["a", "aa"], ["m", [["b", "bb"]]]]);

Pls if you don't understand this, please go through again, and if you know you understand and there's a way you can put it in a more understandable way, Pls let's know.

In a nutshell:

// From
const m = new Map([["a", "aa"], ["m", new Map([["b", "bb"], ["mm", new Map()]])]]);

// through
[...m]

// to 
[
["a", "aa"],
["m", [["b", "bb"], ["mm", []]]],
]  

I've not gone deep into using symbols in metaprogramming, if this'll be possible by metaprogramming my way through, let me know.

Thanks for reading through. :heart:

Something like this seems to work:

console.clear();

function stringifyMap(key, value) {
  if (value instanceof Map) {
    return { $map: [...value] };
  } else {
    return value;
  }
}

function parseMap(key, value) {
  if (key === '$map') {
    return new Map(value);
  }
  if (typeof value === 'object' && value != null && value.$map) {
      return value.$map;
  }
  return value;
}

const m = new Map([["a", "aa"], ["m", new Map([["b", "bb"], ["mm", new Map()]])]]);

let stringMap = JSON.stringify(m, stringifyMap);
console.log('stringMap:', stringMap)

const m2 = JSON.parse(stringMap, parseMap);
console.log('m2', m2);

I had my way around it too, but we'll be glad with such feature directly in JavaScript isn't it? It eases the work, and allows for more possibilities... Consider the time before we had optional chaining.

It wouldn't be possible to change the current behavior of [...new Map(entries)] as this would break existing code.

You can create a subclass of Map that has the nesting behaviour that you are after:

class NestedMap extends Map {
  *[Symbol.iterator]() {
    for (const [key, value] of this.entries()) {
      if (value instanceof Map) {
        yield [key, [...new NestedMap(value)]]
      } else {
        yield [key, value];
      }
    }
  }
}

const m = new NestedMap([["a", "aa"], ["m", new Map([["b", "bb"], ["mm", new Map()]])]]);

const arr = [...m];

console.log(arr); // [["a","aa"],["m",[["b","bb"],["mm",[]]]]]
1 Like

Cool :clap:, you just helped me finish the Algorithm I gonna be implementing. That's awesome. I agree.

1 Like

I've also defined another style of having entries go in the constructor of the NestedMap class.

So that, when you spread the NestedMap, it works this way, in turn when you pass this result as an argument to another Nested Map constructor, it reverses the case, just like the normal Nested Map before it is spread.

Two features I need on my software. :wink: