2

I am developing an Angular app. I use Typescript and have a certain class which contains the built-in Set and a Map types. My problem is that I want to store instances of my class in the browser's localStorage as JSON, but when I try to stringify them I get empty strings for these types.

Example from chrom's console:

> inst = new Object({
    'elem': new Set(),
    'str': 'somestring'
})
: Object {elem: Set(0), str: "somestring"}elem: Set(0)str: "somestring"}
> inst.elem.add('123');
: Set(1) {"123"}
> inst.elem.add('123');
: Set(1) {"123"}size: (...)__proto__: Set[[Entries]]: Array(1)0: "123"length: 1
> JSON.stringify(inst)
: "{"elem":{},"str":"somestring"}"

The only thing I could think of is recursively converting Sets and Maps to Arrays and Objects respectively before stringifying. But it sounds like much effort.

Is there a better way? Thanks.

Touffy
  • 6,309
  • 22
  • 28
ronenmiller
  • 1,117
  • 15
  • 25
  • @Bergi I'd like to point out that parts of this question are not answered there. Do you think OP should really ask a new one, or maybe refine his title to make it clearer that nested iterables are an issue? – Touffy Apr 30 '17 at 21:23
  • @Touffy Thanks, I didn't notice before, strictly that's a different question. I guess asking a new one isn't necessary as you already answered it perfectly. – Bergi Apr 30 '17 at 23:42

1 Answers1

10

Converting a Map to a simple Object won't work, unless the keys happen to be all strings. Remember that Maps can have anything as keys, including things that are not equal by reference but that will result in the same string when coerced.

The obvious way to serialise Maps and Sets would be, indeed, to convert them to arrays and then serialise that. Fortunately, you don't have to write much code yourself, just use the built-in iterators:

const map = new Map([['a', 1], ['b', 2]])
const set = new Set([2,3,5,7,11])

serializedMap = JSON.stringify([...map]) // => [["a", 1], ["b", 2]]
serializedSet = JSON.stringify([...set]) // => [2,3,5,7,11]

Now, as you noticed, you'll have some trouble with nested Maps and Sets. To avoid writing specific code to dive into objects and turn their deeply nested Map and Set values into arrays, you can define the toJSON method on Map and Set prototypes. The method is called implicitly by JSON.stringify:

Map.prototype.toJSON = function () {
    return [...this]
}
// or, if you really want to use objects:
Map.prototype.toJSON = function () {
    var obj = {}
    for(let [key, value] of this)
        obj[key] = value
    return obj
}

// and for Sets:
Set.prototype.toJSON = function () {
    return [...this]
}

Be careful, toJSON may be defined for Sets and Maps sometime in the future, with a different behaviour than this. Although it seems unlikely.

Now, whenever you call JSON.stringify and it sees a Map or Set, it will notice the toJSON method and use it instead of just copying the object's properties.

Another, similar and more ECMA-approved solution is to use JSON.stringify's second argument. Define a helper function that will preprocess values, replacing Maps and Sets with appropriate arrays or objects:

function mapReplacer(key, value) {
    if(value instanceof Map || value instanceof Set) {
        return [...value]
        // of course you can separate cases to turn Maps into objects
    }
    return value
}

Now just pass mapReplacer to JSON.stringify:

JSON.stringify(map, mapReplacer)
Touffy
  • 6,309
  • 22
  • 28
  • What would I do with a member of the following type: `Map>` – ronenmiller Apr 30 '17 at 14:11
  • Good point. I've expanded my answer to show you how to solve that using the "magic" of `JSON.stringify` itself. – Touffy Apr 30 '17 at 21:16
  • Great! Both solutions are good options and seem easy to use. Thanks a lot! – ronenmiller May 01 '17 at 11:15
  • 1
    Is there a similar way to do this for JSON.parse()? nvm...found one: https://gist.github.com/allenwb/3837490....just call it w/ JSON.parse(jsonblob, mapreviver). This really should just be built into ES6 :-P – kenyee Feb 01 '19 at 18:30