ES6 Map autoconverted to {}? [duplicate] - javascript

I'd like to start using ES6 Map instead of JS objects but I'm being held back because I can't figure out how to JSON.stringify() a Map. My keys are guaranteed to be strings and my values will always be listed. Do I really have to write a wrapper method to serialize?

Both JSON.stringify and JSON.parse support a second argument. replacer and reviver respectively. With replacer and reviver below it's possible to add support for native Map object, including deeply nested values
function replacer(key, value) {
if(value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else {
return value;
}
}
function reviver(key, value) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
}
}
return value;
}
Usage:
const originalValue = new Map([['a', 1]]);
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
Deep nesting with combination of Arrays, Objects and Maps
const originalValue = [
new Map([['a', {
b: {
c: new Map([['d', 'text']])
}
}]])
];
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);

You can't directly stringify the Map instance as it doesn't have any properties, but you can convert it to an array of tuples:
jsonText = JSON.stringify(Array.from(map.entries()));
For the reverse, use
map = new Map(JSON.parse(jsonText));

You can't.
The keys of a map can be anything, including objects. But JSON syntax only allows strings as keys. So it's impossible in a general case.
My keys are guaranteed to be strings and my values will always be lists
In this case, you can use a plain object. It will have these advantages:
It will be able to be stringified to JSON.
It will work on older browsers.
It might be faster.

While there is no method provided by ecmascript yet, this can still be done using JSON.stingify if you map the Map to a JavaScript primitive. Here is the sample Map we'll use.
const map = new Map();
map.set('foo', 'bar');
map.set('baz', 'quz');
Going to an JavaScript Object
You can convert to JavaScript Object literal with the following helper function.
const mapToObj = m => {
return Array.from(m).reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
};
JSON.stringify(mapToObj(map)); // '{"foo":"bar","baz":"quz"}'
Going to a JavaScript Array of Objects
The helper function for this one would be even more compact
const mapToAoO = m => {
return Array.from(m).map( ([k,v]) => {return {[k]:v}} );
};
JSON.stringify(mapToAoO(map)); // '[{"foo":"bar"},{"baz":"quz"}]'
Going to Array of Arrays
This is even easier, you can just use
JSON.stringify( Array.from(map) ); // '[["foo","bar"],["baz","quz"]]'

Using spread sytax Map can be serialized in one line:
JSON.stringify([...new Map()]);
and deserialize it with:
let map = new Map(JSON.parse(map));

Given your example is a simple use case in which keys are going to be simple types, I think this is the easiest way to JSON stringify a Map.
JSON.stringify(Object.fromEntries(map));
The way I think about the underlying data structure of a Map is as an array of key-value pairs (as arrays themselves). So, something like this:
const myMap = new Map([
["key1", "value1"],
["key2", "value2"],
["key3", "value3"]
]);
Because that underlying data structure is what we find in Object.entries, we can utilize the native JavaScript method of Object.fromEntries() on a Map as we would on an Array:
Object.fromEntries(myMap);
/*
{
key1: "value1",
key2: "value2",
key3: "value3"
}
*/
And then all you're left with is using JSON.stringify() on the result of that.

A Better Solution
// somewhere...
class Klass extends Map {
toJSON() {
var object = { };
for (let [key, value] of this) object[key] = value;
return object;
}
}
// somewhere else...
import { Klass as Map } from '#core/utilities/ds/map'; // <--wherever "somewhere" is
var map = new Map();
map.set('a', 1);
map.set('b', { datum: true });
map.set('c', [ 1,2,3 ]);
map.set( 'd', new Map([ ['e', true] ]) );
var json = JSON.stringify(map, null, '\t');
console.log('>', json);
Output
> {
"a": 1,
"b": {
"datum": true
},
"c": [
1,
2,
3
],
"d": {
"e": true
}
}
Hope that is less cringey than the answers above.

Stringify a Map instance (objects as keys are OK):
JSON.stringify([...map])
or
JSON.stringify(Array.from(map))
or
JSON.stringify(Array.from(map.entries()))
output format:
// [["key1","value1"],["key2","value2"]]

Below solution works even if you have nested Maps
function stringifyMap(myMap) {
function selfIterator(map) {
return Array.from(map).reduce((acc, [key, value]) => {
if (value instanceof Map) {
acc[key] = selfIterator(value);
} else {
acc[key] = value;
}
return acc;
}, {})
}
const res = selfIterator(myMap)
return JSON.stringify(res);
}

The very simple way.
const map = new Map();
map.set('Key1', "Value1");
map.set('Key2', "Value2");
console.log(Object.fromEntries(map));
`
Output:-
{"Key1": "Value1","Key2": "Value2"}

Just want to share my version for both Map and Set JSON.stringify only.
I'm sorting them, useful for debugging...
function replacer(key, value) {
if (value instanceof Map) {
const reducer = (obj, mapKey) => {
obj[mapKey] = value.get(mapKey);
return obj;
};
return [...value.keys()].sort().reduce(reducer, {});
} else if (value instanceof Set) {
return [...value].sort();
}
return value;
}
Usage:
const map = new Map();
const numbers= new Set()
numbers.add(3);
numbers.add(2);
numbers.add(3);
numbers.add(1);
const chars= new Set()
chars.add('b')
chars.add('a')
chars.add('a')
map.set("numbers",numbers)
map.set("chars",chars)
console.log(JSON.stringify(map, replacer, 2));
Result:
{
"chars": [
"a",
"b"
],
"numbers": [
1,
2,
3
]
}

You cannot call JSON.stringify on Map or Set.
You will need to convert:
the Map into a primitive Object, using Object.fromEntries, or
the Set into a primitive Array, using the spread operator [...]
…before calling JSON.stringify
Map
const
obj = { 'Key1': 'Value1', 'Key2': 'Value2' },
map = new Map(Object.entries(obj));
map.set('Key3', 'Value3'); // Add a new entry
// Does NOT show the key-value pairs
console.log('Map:', JSON.stringify(map));
// Shows the key-value pairs
console.log(JSON.stringify(Object.fromEntries(map), null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Set
const
arr = ['Value1', 'Value2'],
set = new Set(arr);
set.add('Value3'); // Add a new item
// Does NOT show the values
console.log('Set:', JSON.stringify(set));
// Show the values
console.log(JSON.stringify([...set], null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
toJSON method
If you want to call JSON.stringify on a class object, you will need to override the toJSON method to return your instance data.
class Cat {
constructor(options = {}) {
this.name = options.name ?? '';
this.age = options.age ?? 0;
}
toString() {
return `[Cat name="${this.name}", age="${this.age}"]`
}
toJSON() {
return { name: this.name, age: this.age };
}
static fromObject(obj) {
const { name, age } = obj ?? {};
return new Cat({ name, age });
}
}
/*
* JSON Set adds the missing methods:
* - toJSON
* - toString
*/
class JSONSet extends Set {
constructor(values) {
super(values)
}
toString() {
return super
.toString()
.replace(']', ` ${[...this].map(v => v.toString())
.join(', ')}]`);
}
toJSON() {
return [...this];
}
}
const cats = new JSONSet([
Cat.fromObject({ name: 'Furball', age: 2 }),
Cat.fromObject({ name: 'Artemis', age: 5 })
]);
console.log(cats.toString());
console.log(JSON.stringify(cats, null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }

Correctly round-tripping serialization
Just copy this and use it. Or use the npm package.
const serialize = (value) => JSON.stringify(value, stringifyReplacer);
const deserialize = (text) => JSON.parse(text, parseReviver);
// License: CC0
function stringifyReplacer(key, value) {
if (typeof value === "object" && value !== null) {
if (value instanceof Map) {
return {
_meta: { type: "map" },
value: Array.from(value.entries()),
};
} else if (value instanceof Set) { // bonus feature!
return {
_meta: { type: "set" },
value: Array.from(value.values()),
};
} else if ("_meta" in value) {
// Escape "_meta" properties
return {
...value,
_meta: {
type: "escaped-meta",
value: value["_meta"],
},
};
}
}
return value;
}
function parseReviver(key, value) {
if (typeof value === "object" && value !== null) {
if ("_meta" in value) {
if (value._meta.type === "map") {
return new Map(value.value);
} else if (value._meta.type === "set") {
return new Set(value.value);
} else if (value._meta.type === "escaped-meta") {
// Un-escape the "_meta" property
return {
...value,
_meta: value._meta.value,
};
} else {
console.warn("Unexpected meta", value._meta);
}
}
}
return value;
}
Why is this hard?
It should be possible to input any kind of data, get valid JSON, and from there correctly reconstruct the input.
This means dealing with
Maps that have objects as keys new Map([ [{cat:1}, "value"] ]). This means that any answer which uses Object.fromEntries is probably wrong.
Maps that have nested maps new Map([ ["key", new Map([ ["nested key", "nested value"] ])] ]). A lot of answers sidestep this by only answering the question and not dealing with anything beyond that.
Mixing objects and maps {"key": new Map([ ["nested key", "nested value"] ]) }.
and on top of those difficulties, the serialisation format must be unambiguous. Otherwise one cannot always reconstruct the input. The top answer has one failing test case, see below.
Hence, I wrote this improved version. It uses _meta instead of dataType, to make conflicts rarer and if a conflict does happen, it actually unambiguously handles it. Hopefully the code is also simple enough to easily be extended to handle other containers.
My answer does, however, not attempt to handle exceedingly cursed cases, such as a map with object properties.
A test case for my answer, which demonstrates a few edge cases
const originalValue = [
new Map([['a', {
b: {
_meta: { __meta: "cat" },
c: new Map([['d', 'text']])
}
}]]),
{ _meta: { type: "map" }}
];
console.log(originalValue);
let text = JSON.stringify(originalValue, stringifyReplacer);
console.log(text);
console.log(JSON.parse(text, parseReviver));
Accepted answer not round-tripping
The accepted answer is really lovely. However, it does not round trip when an object with a dataType property is passed it it.
// Test case for the accepted answer
const originalValue = { dataType: "Map" };
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, str, newValue);
// > Object { dataType: "Map" } , Map(0)
// Notice how the input was changed into something different

I really don't know why there are so many long awesers here. This short version solved my problem:
const data = new Map()
data.set('visible', true)
data.set('child', new Map())
data.get('child').set('visible', false)
const str = JSON.stringify(data, (_, v) => v instanceof Map ? Object.fromEntries(v) : v)
// '{"visible":true,"child":{"visible":false}}'
const recovered = JSON.parse(str, (_, v) => typeof v === 'object' ? new Map(Object.entries(v)) : v)
// Map(2) { 'visible' => true, 'child' => Map(1) { 'visible' => false } }

The following method will convert a Map to a JSON string:
public static getJSONObj(): string {
return JSON.stringify(Object.fromEntries(map));
}
Example:
const x = new Map();
x.set("SomeBool", true);
x.set("number1", 1);
x.set("anObj", { name: "joe", age: 22, isAlive: true });
const json = getJSONObj(x);
// Output:
// '{"SomeBool":true,"number1":1,"anObj":{"name":"joe","age":222,"isAlive":true}}'

Although there would be some scenarios where if you were the creator of the map you would write your code in a separate 'src' file and save a copy as a .txt file and, if written concisely enough, could easily be read in, deciphered, and added to server-side.
The new file would then be saved as a .js and a reference to it sent back from the server. The file would then reconstruct itself perfectly once read back in as JS. The beauty being that no hacky iterating or parsing is required for reconstruction.

Related

How to check if an object has other than specific properties

I have an object obj which has n number of possible properties
lets say some of them are known,
const someKnownProps = ["props.abc", "xyz"]; // or more
I want to know if obj has other than known properties in it.
To clarify:
obj can look like this:
obj = {
props: {
abc: {
def: 1
},
ghi: {
jkl: 2
}
},
xyz: 3
}
Doing Object.keys only return first level children,
in this case it will return props not props.abc
You can use Object.keys to get all keys and filter the keys which aren't included in the someKnownProps array.
const obj = {
"props.abc": 1,
"xyz": 2,
"three": 3,
"four": 4,
}
const someKnownProps = ["props.abc", "xyz"]; // or more
const unknownKeys = Object.keys(obj).filter(key => !someKnownProps.includes(key))
console.log(unknownKeys)
There are two (unrelated) tasks involved in this question:
Traversal of an object's properties
Comparison of a set of traversed object properties to a list of strings representing dot-notation-formatted object property accessors
While I'm sure the former has been previously discussed on SO, I'll provide an implementation of such an algorithm below in order to address the details of this question.
This is essentially a specific case of recursion where each cycle starts with these inputs:
an object
a dot-notation-formatted path
a Set of existing such paths
The code below includes inline comments explaining what's happening, and there are some console.log statements at the end to help you visualize some example results based on the data in your question. If something is unclear after reviewing the code, feel free to leave a comment.
'use strict';
/** #returns whether value is a non-null, non-array object */
function isObject (value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
/** #returns the enumerable (optionally including inherited) keys of an object */
function getKeys (obj, includeInherited = false) {
if (!includeInherited) return Object.keys(obj);
const keys = new Set();
let o = obj;
while (o !== null) {
for (const key of Object.keys(o)) keys.add(key);
o = Object.getPrototypeOf(o);
}
return [...keys];
}
/**
* #returns an array of strings representing all traversible branches
* of child objects, each formatted as a combined path of dot-notation
* property accessors
*/
function findObjectPaths (
obj,
{
includeInherited = false,
currentPath = '',
paths = new Set(),
skipReturn = false,
} = {},
) {
for (const key of getKeys(obj, includeInherited)) {
// Append the current dot-notation property accessor
// to the existing path of this object:
const path = `${currentPath}.${key}`;
// Add it to the set:
paths.add(path);
const o = obj[key];
// Recurse if the child value is an object:
if (isObject(o)) {
findObjectPaths(o, {
includeInherited,
currentPath: path,
paths,
skipReturn: true,
});
}
}
// If this is not a sub-cycle (it's the top-level invocation), then convert
// the set to an array and remove the first "." from each string
if (!skipReturn) return [...paths].map(p => p.slice(1));
}
// Use:
const obj = {
props: {
abc: {
def: 1,
},
ghi: {
jkl: 2,
},
},
xyz: 3,
};
let someKnownProps = ['props.abc', 'xyz'];
let objectPaths = findObjectPaths(obj);
let hasOtherProps = objectPaths.some(path => !someKnownProps.includes(path));
console.log(hasOtherProps); // true
// An example of all of the paths in the object above:
someKnownProps = [
'props',
'props.abc',
'props.abc.def',
'props.ghi',
'props.ghi.jkl',
'xyz',
];
objectPaths = findObjectPaths(obj);
hasOtherProps = objectPaths.some(path => !someKnownProps.includes(path));
console.log(hasOtherProps); // false
// Finally, comparing the results of inherited vs non-inherited enumeration:
const objWithoutOwnProps = Object.create({
props: {
abc: {
def: 1,
},
ghi: {
jkl: 2,
},
},
xyz: 3,
});
console.log(
'Non-inherited props:',
findObjectPaths(objWithoutOwnProps),
);
console.log(
'Inherited props:',
findObjectPaths(objWithoutOwnProps, {includeInherited: true}),
);
Similar to what Mina said:
let obj = {one: 1, two: 2, three: 3};
let knownKeys = ['one', 'two'];
for (let key in obj) {
if (!knownKeys.includes(key)) {
console.log(key);
}
}

How do I write a Map to a text file in JavaScript? [duplicate]

I'd like to start using ES6 Map instead of JS objects but I'm being held back because I can't figure out how to JSON.stringify() a Map. My keys are guaranteed to be strings and my values will always be listed. Do I really have to write a wrapper method to serialize?
Both JSON.stringify and JSON.parse support a second argument. replacer and reviver respectively. With replacer and reviver below it's possible to add support for native Map object, including deeply nested values
function replacer(key, value) {
if(value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else {
return value;
}
}
function reviver(key, value) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
}
}
return value;
}
Usage:
const originalValue = new Map([['a', 1]]);
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
Deep nesting with combination of Arrays, Objects and Maps
const originalValue = [
new Map([['a', {
b: {
c: new Map([['d', 'text']])
}
}]])
];
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
You can't directly stringify the Map instance as it doesn't have any properties, but you can convert it to an array of tuples:
jsonText = JSON.stringify(Array.from(map.entries()));
For the reverse, use
map = new Map(JSON.parse(jsonText));
You can't.
The keys of a map can be anything, including objects. But JSON syntax only allows strings as keys. So it's impossible in a general case.
My keys are guaranteed to be strings and my values will always be lists
In this case, you can use a plain object. It will have these advantages:
It will be able to be stringified to JSON.
It will work on older browsers.
It might be faster.
While there is no method provided by ecmascript yet, this can still be done using JSON.stingify if you map the Map to a JavaScript primitive. Here is the sample Map we'll use.
const map = new Map();
map.set('foo', 'bar');
map.set('baz', 'quz');
Going to an JavaScript Object
You can convert to JavaScript Object literal with the following helper function.
const mapToObj = m => {
return Array.from(m).reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
};
JSON.stringify(mapToObj(map)); // '{"foo":"bar","baz":"quz"}'
Going to a JavaScript Array of Objects
The helper function for this one would be even more compact
const mapToAoO = m => {
return Array.from(m).map( ([k,v]) => {return {[k]:v}} );
};
JSON.stringify(mapToAoO(map)); // '[{"foo":"bar"},{"baz":"quz"}]'
Going to Array of Arrays
This is even easier, you can just use
JSON.stringify( Array.from(map) ); // '[["foo","bar"],["baz","quz"]]'
Using spread sytax Map can be serialized in one line:
JSON.stringify([...new Map()]);
and deserialize it with:
let map = new Map(JSON.parse(map));
Given your example is a simple use case in which keys are going to be simple types, I think this is the easiest way to JSON stringify a Map.
JSON.stringify(Object.fromEntries(map));
The way I think about the underlying data structure of a Map is as an array of key-value pairs (as arrays themselves). So, something like this:
const myMap = new Map([
["key1", "value1"],
["key2", "value2"],
["key3", "value3"]
]);
Because that underlying data structure is what we find in Object.entries, we can utilize the native JavaScript method of Object.fromEntries() on a Map as we would on an Array:
Object.fromEntries(myMap);
/*
{
key1: "value1",
key2: "value2",
key3: "value3"
}
*/
And then all you're left with is using JSON.stringify() on the result of that.
A Better Solution
// somewhere...
class Klass extends Map {
toJSON() {
var object = { };
for (let [key, value] of this) object[key] = value;
return object;
}
}
// somewhere else...
import { Klass as Map } from '#core/utilities/ds/map'; // <--wherever "somewhere" is
var map = new Map();
map.set('a', 1);
map.set('b', { datum: true });
map.set('c', [ 1,2,3 ]);
map.set( 'd', new Map([ ['e', true] ]) );
var json = JSON.stringify(map, null, '\t');
console.log('>', json);
Output
> {
"a": 1,
"b": {
"datum": true
},
"c": [
1,
2,
3
],
"d": {
"e": true
}
}
Hope that is less cringey than the answers above.
Stringify a Map instance (objects as keys are OK):
JSON.stringify([...map])
or
JSON.stringify(Array.from(map))
or
JSON.stringify(Array.from(map.entries()))
output format:
// [["key1","value1"],["key2","value2"]]
Below solution works even if you have nested Maps
function stringifyMap(myMap) {
function selfIterator(map) {
return Array.from(map).reduce((acc, [key, value]) => {
if (value instanceof Map) {
acc[key] = selfIterator(value);
} else {
acc[key] = value;
}
return acc;
}, {})
}
const res = selfIterator(myMap)
return JSON.stringify(res);
}
The very simple way.
const map = new Map();
map.set('Key1', "Value1");
map.set('Key2', "Value2");
console.log(Object.fromEntries(map));
`
Output:-
{"Key1": "Value1","Key2": "Value2"}
Just want to share my version for both Map and Set JSON.stringify only.
I'm sorting them, useful for debugging...
function replacer(key, value) {
if (value instanceof Map) {
const reducer = (obj, mapKey) => {
obj[mapKey] = value.get(mapKey);
return obj;
};
return [...value.keys()].sort().reduce(reducer, {});
} else if (value instanceof Set) {
return [...value].sort();
}
return value;
}
Usage:
const map = new Map();
const numbers= new Set()
numbers.add(3);
numbers.add(2);
numbers.add(3);
numbers.add(1);
const chars= new Set()
chars.add('b')
chars.add('a')
chars.add('a')
map.set("numbers",numbers)
map.set("chars",chars)
console.log(JSON.stringify(map, replacer, 2));
Result:
{
"chars": [
"a",
"b"
],
"numbers": [
1,
2,
3
]
}
You cannot call JSON.stringify on Map or Set.
You will need to convert:
the Map into a primitive Object, using Object.fromEntries, or
the Set into a primitive Array, using the spread operator [...]
…before calling JSON.stringify
Map
const
obj = { 'Key1': 'Value1', 'Key2': 'Value2' },
map = new Map(Object.entries(obj));
map.set('Key3', 'Value3'); // Add a new entry
// Does NOT show the key-value pairs
console.log('Map:', JSON.stringify(map));
// Shows the key-value pairs
console.log(JSON.stringify(Object.fromEntries(map), null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Set
const
arr = ['Value1', 'Value2'],
set = new Set(arr);
set.add('Value3'); // Add a new item
// Does NOT show the values
console.log('Set:', JSON.stringify(set));
// Show the values
console.log(JSON.stringify([...set], null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
toJSON method
If you want to call JSON.stringify on a class object, you will need to override the toJSON method to return your instance data.
class Cat {
constructor(options = {}) {
this.name = options.name ?? '';
this.age = options.age ?? 0;
}
toString() {
return `[Cat name="${this.name}", age="${this.age}"]`
}
toJSON() {
return { name: this.name, age: this.age };
}
static fromObject(obj) {
const { name, age } = obj ?? {};
return new Cat({ name, age });
}
}
/*
* JSON Set adds the missing methods:
* - toJSON
* - toString
*/
class JSONSet extends Set {
constructor(values) {
super(values)
}
toString() {
return super
.toString()
.replace(']', ` ${[...this].map(v => v.toString())
.join(', ')}]`);
}
toJSON() {
return [...this];
}
}
const cats = new JSONSet([
Cat.fromObject({ name: 'Furball', age: 2 }),
Cat.fromObject({ name: 'Artemis', age: 5 })
]);
console.log(cats.toString());
console.log(JSON.stringify(cats, null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Correctly round-tripping serialization
Just copy this and use it. Or use the npm package.
const serialize = (value) => JSON.stringify(value, stringifyReplacer);
const deserialize = (text) => JSON.parse(text, parseReviver);
// License: CC0
function stringifyReplacer(key, value) {
if (typeof value === "object" && value !== null) {
if (value instanceof Map) {
return {
_meta: { type: "map" },
value: Array.from(value.entries()),
};
} else if (value instanceof Set) { // bonus feature!
return {
_meta: { type: "set" },
value: Array.from(value.values()),
};
} else if ("_meta" in value) {
// Escape "_meta" properties
return {
...value,
_meta: {
type: "escaped-meta",
value: value["_meta"],
},
};
}
}
return value;
}
function parseReviver(key, value) {
if (typeof value === "object" && value !== null) {
if ("_meta" in value) {
if (value._meta.type === "map") {
return new Map(value.value);
} else if (value._meta.type === "set") {
return new Set(value.value);
} else if (value._meta.type === "escaped-meta") {
// Un-escape the "_meta" property
return {
...value,
_meta: value._meta.value,
};
} else {
console.warn("Unexpected meta", value._meta);
}
}
}
return value;
}
Why is this hard?
It should be possible to input any kind of data, get valid JSON, and from there correctly reconstruct the input.
This means dealing with
Maps that have objects as keys new Map([ [{cat:1}, "value"] ]). This means that any answer which uses Object.fromEntries is probably wrong.
Maps that have nested maps new Map([ ["key", new Map([ ["nested key", "nested value"] ])] ]). A lot of answers sidestep this by only answering the question and not dealing with anything beyond that.
Mixing objects and maps {"key": new Map([ ["nested key", "nested value"] ]) }.
and on top of those difficulties, the serialisation format must be unambiguous. Otherwise one cannot always reconstruct the input. The top answer has one failing test case, see below.
Hence, I wrote this improved version. It uses _meta instead of dataType, to make conflicts rarer and if a conflict does happen, it actually unambiguously handles it. Hopefully the code is also simple enough to easily be extended to handle other containers.
My answer does, however, not attempt to handle exceedingly cursed cases, such as a map with object properties.
A test case for my answer, which demonstrates a few edge cases
const originalValue = [
new Map([['a', {
b: {
_meta: { __meta: "cat" },
c: new Map([['d', 'text']])
}
}]]),
{ _meta: { type: "map" }}
];
console.log(originalValue);
let text = JSON.stringify(originalValue, stringifyReplacer);
console.log(text);
console.log(JSON.parse(text, parseReviver));
Accepted answer not round-tripping
The accepted answer is really lovely. However, it does not round trip when an object with a dataType property is passed it it.
// Test case for the accepted answer
const originalValue = { dataType: "Map" };
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, str, newValue);
// > Object { dataType: "Map" } , Map(0)
// Notice how the input was changed into something different
I really don't know why there are so many long awesers here. This short version solved my problem:
const data = new Map()
data.set('visible', true)
data.set('child', new Map())
data.get('child').set('visible', false)
const str = JSON.stringify(data, (_, v) => v instanceof Map ? Object.fromEntries(v) : v)
// '{"visible":true,"child":{"visible":false}}'
const recovered = JSON.parse(str, (_, v) => typeof v === 'object' ? new Map(Object.entries(v)) : v)
// Map(2) { 'visible' => true, 'child' => Map(1) { 'visible' => false } }
The following method will convert a Map to a JSON string:
public static getJSONObj(): string {
return JSON.stringify(Object.fromEntries(map));
}
Example:
const x = new Map();
x.set("SomeBool", true);
x.set("number1", 1);
x.set("anObj", { name: "joe", age: 22, isAlive: true });
const json = getJSONObj(x);
// Output:
// '{"SomeBool":true,"number1":1,"anObj":{"name":"joe","age":222,"isAlive":true}}'
Although there would be some scenarios where if you were the creator of the map you would write your code in a separate 'src' file and save a copy as a .txt file and, if written concisely enough, could easily be read in, deciphered, and added to server-side.
The new file would then be saved as a .js and a reference to it sent back from the server. The file would then reconstruct itself perfectly once read back in as JS. The beauty being that no hacky iterating or parsing is required for reconstruction.

How to use map to insert key-value pair in google appScript in this unique case? [duplicate]

I'd like to start using ES6 Map instead of JS objects but I'm being held back because I can't figure out how to JSON.stringify() a Map. My keys are guaranteed to be strings and my values will always be listed. Do I really have to write a wrapper method to serialize?
Both JSON.stringify and JSON.parse support a second argument. replacer and reviver respectively. With replacer and reviver below it's possible to add support for native Map object, including deeply nested values
function replacer(key, value) {
if(value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else {
return value;
}
}
function reviver(key, value) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
}
}
return value;
}
Usage:
const originalValue = new Map([['a', 1]]);
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
Deep nesting with combination of Arrays, Objects and Maps
const originalValue = [
new Map([['a', {
b: {
c: new Map([['d', 'text']])
}
}]])
];
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
You can't directly stringify the Map instance as it doesn't have any properties, but you can convert it to an array of tuples:
jsonText = JSON.stringify(Array.from(map.entries()));
For the reverse, use
map = new Map(JSON.parse(jsonText));
You can't.
The keys of a map can be anything, including objects. But JSON syntax only allows strings as keys. So it's impossible in a general case.
My keys are guaranteed to be strings and my values will always be lists
In this case, you can use a plain object. It will have these advantages:
It will be able to be stringified to JSON.
It will work on older browsers.
It might be faster.
While there is no method provided by ecmascript yet, this can still be done using JSON.stingify if you map the Map to a JavaScript primitive. Here is the sample Map we'll use.
const map = new Map();
map.set('foo', 'bar');
map.set('baz', 'quz');
Going to an JavaScript Object
You can convert to JavaScript Object literal with the following helper function.
const mapToObj = m => {
return Array.from(m).reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
};
JSON.stringify(mapToObj(map)); // '{"foo":"bar","baz":"quz"}'
Going to a JavaScript Array of Objects
The helper function for this one would be even more compact
const mapToAoO = m => {
return Array.from(m).map( ([k,v]) => {return {[k]:v}} );
};
JSON.stringify(mapToAoO(map)); // '[{"foo":"bar"},{"baz":"quz"}]'
Going to Array of Arrays
This is even easier, you can just use
JSON.stringify( Array.from(map) ); // '[["foo","bar"],["baz","quz"]]'
Using spread sytax Map can be serialized in one line:
JSON.stringify([...new Map()]);
and deserialize it with:
let map = new Map(JSON.parse(map));
Given your example is a simple use case in which keys are going to be simple types, I think this is the easiest way to JSON stringify a Map.
JSON.stringify(Object.fromEntries(map));
The way I think about the underlying data structure of a Map is as an array of key-value pairs (as arrays themselves). So, something like this:
const myMap = new Map([
["key1", "value1"],
["key2", "value2"],
["key3", "value3"]
]);
Because that underlying data structure is what we find in Object.entries, we can utilize the native JavaScript method of Object.fromEntries() on a Map as we would on an Array:
Object.fromEntries(myMap);
/*
{
key1: "value1",
key2: "value2",
key3: "value3"
}
*/
And then all you're left with is using JSON.stringify() on the result of that.
A Better Solution
// somewhere...
class Klass extends Map {
toJSON() {
var object = { };
for (let [key, value] of this) object[key] = value;
return object;
}
}
// somewhere else...
import { Klass as Map } from '#core/utilities/ds/map'; // <--wherever "somewhere" is
var map = new Map();
map.set('a', 1);
map.set('b', { datum: true });
map.set('c', [ 1,2,3 ]);
map.set( 'd', new Map([ ['e', true] ]) );
var json = JSON.stringify(map, null, '\t');
console.log('>', json);
Output
> {
"a": 1,
"b": {
"datum": true
},
"c": [
1,
2,
3
],
"d": {
"e": true
}
}
Hope that is less cringey than the answers above.
Stringify a Map instance (objects as keys are OK):
JSON.stringify([...map])
or
JSON.stringify(Array.from(map))
or
JSON.stringify(Array.from(map.entries()))
output format:
// [["key1","value1"],["key2","value2"]]
Below solution works even if you have nested Maps
function stringifyMap(myMap) {
function selfIterator(map) {
return Array.from(map).reduce((acc, [key, value]) => {
if (value instanceof Map) {
acc[key] = selfIterator(value);
} else {
acc[key] = value;
}
return acc;
}, {})
}
const res = selfIterator(myMap)
return JSON.stringify(res);
}
The very simple way.
const map = new Map();
map.set('Key1', "Value1");
map.set('Key2', "Value2");
console.log(Object.fromEntries(map));
`
Output:-
{"Key1": "Value1","Key2": "Value2"}
Just want to share my version for both Map and Set JSON.stringify only.
I'm sorting them, useful for debugging...
function replacer(key, value) {
if (value instanceof Map) {
const reducer = (obj, mapKey) => {
obj[mapKey] = value.get(mapKey);
return obj;
};
return [...value.keys()].sort().reduce(reducer, {});
} else if (value instanceof Set) {
return [...value].sort();
}
return value;
}
Usage:
const map = new Map();
const numbers= new Set()
numbers.add(3);
numbers.add(2);
numbers.add(3);
numbers.add(1);
const chars= new Set()
chars.add('b')
chars.add('a')
chars.add('a')
map.set("numbers",numbers)
map.set("chars",chars)
console.log(JSON.stringify(map, replacer, 2));
Result:
{
"chars": [
"a",
"b"
],
"numbers": [
1,
2,
3
]
}
You cannot call JSON.stringify on Map or Set.
You will need to convert:
the Map into a primitive Object, using Object.fromEntries, or
the Set into a primitive Array, using the spread operator [...]
…before calling JSON.stringify
Map
const
obj = { 'Key1': 'Value1', 'Key2': 'Value2' },
map = new Map(Object.entries(obj));
map.set('Key3', 'Value3'); // Add a new entry
// Does NOT show the key-value pairs
console.log('Map:', JSON.stringify(map));
// Shows the key-value pairs
console.log(JSON.stringify(Object.fromEntries(map), null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Set
const
arr = ['Value1', 'Value2'],
set = new Set(arr);
set.add('Value3'); // Add a new item
// Does NOT show the values
console.log('Set:', JSON.stringify(set));
// Show the values
console.log(JSON.stringify([...set], null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
toJSON method
If you want to call JSON.stringify on a class object, you will need to override the toJSON method to return your instance data.
class Cat {
constructor(options = {}) {
this.name = options.name ?? '';
this.age = options.age ?? 0;
}
toString() {
return `[Cat name="${this.name}", age="${this.age}"]`
}
toJSON() {
return { name: this.name, age: this.age };
}
static fromObject(obj) {
const { name, age } = obj ?? {};
return new Cat({ name, age });
}
}
/*
* JSON Set adds the missing methods:
* - toJSON
* - toString
*/
class JSONSet extends Set {
constructor(values) {
super(values)
}
toString() {
return super
.toString()
.replace(']', ` ${[...this].map(v => v.toString())
.join(', ')}]`);
}
toJSON() {
return [...this];
}
}
const cats = new JSONSet([
Cat.fromObject({ name: 'Furball', age: 2 }),
Cat.fromObject({ name: 'Artemis', age: 5 })
]);
console.log(cats.toString());
console.log(JSON.stringify(cats, null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Correctly round-tripping serialization
Just copy this and use it. Or use the npm package.
const serialize = (value) => JSON.stringify(value, stringifyReplacer);
const deserialize = (text) => JSON.parse(text, parseReviver);
// License: CC0
function stringifyReplacer(key, value) {
if (typeof value === "object" && value !== null) {
if (value instanceof Map) {
return {
_meta: { type: "map" },
value: Array.from(value.entries()),
};
} else if (value instanceof Set) { // bonus feature!
return {
_meta: { type: "set" },
value: Array.from(value.values()),
};
} else if ("_meta" in value) {
// Escape "_meta" properties
return {
...value,
_meta: {
type: "escaped-meta",
value: value["_meta"],
},
};
}
}
return value;
}
function parseReviver(key, value) {
if (typeof value === "object" && value !== null) {
if ("_meta" in value) {
if (value._meta.type === "map") {
return new Map(value.value);
} else if (value._meta.type === "set") {
return new Set(value.value);
} else if (value._meta.type === "escaped-meta") {
// Un-escape the "_meta" property
return {
...value,
_meta: value._meta.value,
};
} else {
console.warn("Unexpected meta", value._meta);
}
}
}
return value;
}
Why is this hard?
It should be possible to input any kind of data, get valid JSON, and from there correctly reconstruct the input.
This means dealing with
Maps that have objects as keys new Map([ [{cat:1}, "value"] ]). This means that any answer which uses Object.fromEntries is probably wrong.
Maps that have nested maps new Map([ ["key", new Map([ ["nested key", "nested value"] ])] ]). A lot of answers sidestep this by only answering the question and not dealing with anything beyond that.
Mixing objects and maps {"key": new Map([ ["nested key", "nested value"] ]) }.
and on top of those difficulties, the serialisation format must be unambiguous. Otherwise one cannot always reconstruct the input. The top answer has one failing test case, see below.
Hence, I wrote this improved version. It uses _meta instead of dataType, to make conflicts rarer and if a conflict does happen, it actually unambiguously handles it. Hopefully the code is also simple enough to easily be extended to handle other containers.
My answer does, however, not attempt to handle exceedingly cursed cases, such as a map with object properties.
A test case for my answer, which demonstrates a few edge cases
const originalValue = [
new Map([['a', {
b: {
_meta: { __meta: "cat" },
c: new Map([['d', 'text']])
}
}]]),
{ _meta: { type: "map" }}
];
console.log(originalValue);
let text = JSON.stringify(originalValue, stringifyReplacer);
console.log(text);
console.log(JSON.parse(text, parseReviver));
Accepted answer not round-tripping
The accepted answer is really lovely. However, it does not round trip when an object with a dataType property is passed it it.
// Test case for the accepted answer
const originalValue = { dataType: "Map" };
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, str, newValue);
// > Object { dataType: "Map" } , Map(0)
// Notice how the input was changed into something different
I really don't know why there are so many long awesers here. This short version solved my problem:
const data = new Map()
data.set('visible', true)
data.set('child', new Map())
data.get('child').set('visible', false)
const str = JSON.stringify(data, (_, v) => v instanceof Map ? Object.fromEntries(v) : v)
// '{"visible":true,"child":{"visible":false}}'
const recovered = JSON.parse(str, (_, v) => typeof v === 'object' ? new Map(Object.entries(v)) : v)
// Map(2) { 'visible' => true, 'child' => Map(1) { 'visible' => false } }
The following method will convert a Map to a JSON string:
public static getJSONObj(): string {
return JSON.stringify(Object.fromEntries(map));
}
Example:
const x = new Map();
x.set("SomeBool", true);
x.set("number1", 1);
x.set("anObj", { name: "joe", age: 22, isAlive: true });
const json = getJSONObj(x);
// Output:
// '{"SomeBool":true,"number1":1,"anObj":{"name":"joe","age":222,"isAlive":true}}'
Although there would be some scenarios where if you were the creator of the map you would write your code in a separate 'src' file and save a copy as a .txt file and, if written concisely enough, could easily be read in, deciphered, and added to server-side.
The new file would then be saved as a .js and a reference to it sent back from the server. The file would then reconstruct itself perfectly once read back in as JS. The beauty being that no hacky iterating or parsing is required for reconstruction.

Remove underscore prefixes from all keys

I have data that looks like the following:
[
{
"_sourceAddresses":[
{
"_street1":"957 Heathcote Unions",
"_city":"Matteoside",
"_state":"Hawaii",
"_postalCode":"69680",
"_postalCodePlusFour":"7715",
"_country":"USA",
"_type":0,
"_updatedAt":"1991-03-10T22:34:27.000Z",
"_createdAt":"1970-07-24T09:34:12.000Z"
}
],
"_emails":[
{
"_address":"labadie.gwendolyn#gmail.com",
"_primary":true
}
],
"_phoneNumbers":[
{
"_number":"4612902836",
"_type":0,
"_carrier":"AT&T"
}
],
"_customFields":{
},
"_active":true,
"_firstName":"Haven",
"_lastName":"Runolfsdottir",
"_gender":"M",
"_sourceIndividualId":"c1126d05-0e5b-4da1-8535-e1061d4163ee",
"_sourceCampusId":"ae1e70d5-d8bf-4942-b9ea-3da5765e055f",
"_primaryContact":true,
"_salutation":"Mrs.",
"_suffix":"DDS",
"_birthDate":"1989-02-16T10:06:25.000Z"
},
{
"_sourceAddresses":[
{
"_street1":"5910 Langosh Burgs Apt. 281",
"_city":"West Katheryn",
"_state":"Arkansas",
"_postalCode":"49571",
"_postalCodePlusFour":null,
"_country":"USA",
"_type":0,
"_updatedAt":"1984-01-09T09:34:02.000Z",
"_createdAt":"1986-01-13T17:36:41.000Z"
}
],
"_emails":[
{
"_address":"labadie_cristopher#yahoo.com",
"_primary":true
}
],
"_phoneNumbers":[
{
"_number":"0608405498",
"_type":0,
"_carrier":"Verizon"
}
],
"_customFields":{
},
"_active":true,
"_firstName":"Andreane",
"_lastName":"Kerluke",
"_gender":"F",
"_sourceIndividualId":"0726bfc2-56af-4e46-90ef-c0a286404334",
"_sourceCampusId":"86fdb656-7e29-4ace-a1c7-149db81c7f5e",
"_primaryContact":true,
"_salutation":"Mrs.",
"_suffix":null,
"_birthDate":"1979-11-14T10:07:02.000Z"
}
]
When it is saved as JSON, I'd like to remove the underscores in the keys. Is there an easy way to do this?
I have tried unsuccessfully to adapt this code to accomplish it:
Replace dot to underscore in js object keys names
function removeLeadingUnderscores(obj) {
_.forOwn(obj, (value, key) => {
if (_.startsWith("_")) {
const cleanKey = _.substring(1)
obj[cleanKey] = value;
delete obj[key];
}
// continue recursively looping through if we have an object or array
if (_.isObject(value)) {
return removeLeadingUnderscores(value);
}
});
return obj;
}
Since you're planning to save as JSON already, you can use its naturally recursive nature with its reviver parameter to return objects without the underscores. Map the entries of the object to a new object without the leading _.
const arr=[{_sourceAddresses:[{_street1:"957 Heathcote Unions",_city:"Matteoside",_state:"Hawaii",_postalCode:"69680",_postalCodePlusFour:"7715",_country:"USA",_type:0,_updatedAt:"1991-03-10T22:34:27.000Z",_createdAt:"1970-07-24T09:34:12.000Z"}],_emails:[{_address:"labadie.gwendolyn#gmail.com",_primary:!0}],_phoneNumbers:[{_number:"4612902836",_type:0,_carrier:"AT&T"}],_customFields:{},_active:!0,_firstName:"Haven",_lastName:"Runolfsdottir",_gender:"M",_sourceIndividualId:"c1126d05-0e5b-4da1-8535-e1061d4163ee",_sourceCampusId:"ae1e70d5-d8bf-4942-b9ea-3da5765e055f",_primaryContact:!0,_salutation:"Mrs.",_suffix:"DDS",_birthDate:"1989-02-16T10:06:25.000Z"},{_sourceAddresses:[{_street1:"5910 Langosh Burgs Apt. 281",_city:"West Katheryn",_state:"Arkansas",_postalCode:"49571",_postalCodePlusFour:null,_country:"USA",_type:0,_updatedAt:"1984-01-09T09:34:02.000Z",_createdAt:"1986-01-13T17:36:41.000Z"}],_emails:[{_address:"labadie_cristopher#yahoo.com",_primary:!0}],_phoneNumbers:[{_number:"0608405498",_type:0,_carrier:"Verizon"}],_customFields:{},_active:!0,_firstName:"Andreane",_lastName:"Kerluke",_gender:"F",_sourceIndividualId:"0726bfc2-56af-4e46-90ef-c0a286404334",_sourceCampusId:"86fdb656-7e29-4ace-a1c7-149db81c7f5e",_primaryContact:!0,_salutation:"Mrs.",_suffix:null,_birthDate:"1979-11-14T10:07:02.000Z"}];
const stringified = JSON.stringify(
arr,
(_, value) => {
return value && typeof value === 'object' && !Array.isArray(value)
? Object.fromEntries(
Object.entries(value)
.map(([key, value]) => [key.slice(1), value])
)
: value;
}
);
console.log(stringified);
If some properties don't start with _, you can change .slice(1) to .replace(/^_/, '').
Here's a simplified version of saving the object with removed underscores through simple recursive logic:
let savedJson: any = [];
renamingArray(obj); // obj is your object
function renamingArray(element: any){
for(let element of obj)
if (Object.prototype.toString.call(element) === '[object Array]') {
renamingArray(element);
else
renamingObject(element);
}
}
function renamingObject(obj: any){
let keys = Object.keys(obj)
for(let objectKey of keys){
savedJson.push({ [objectKey.substring(1)]: obj[objectKey] });
}
}
console.log(savedJson)

Multiple key names, same pair value

I'm trying to setup an object literal in a JavaScript script that has a key with multiple names. referring to the same object value i.e. something like these that I have already tried:
var holidays: {
"thanksgiving day", "thanksgiving", "t-day": {
someValue : "foo"
}
}
var holidays: {
["thanksgiving day", "thanksgiving", "t-day"]: {
someValue : "foo"
}
}
Is there a way I can accomplish this?
Another approach is to do some postprocessing
function expand(obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; ++i) {
var key = keys[i],
subkeys = key.split(/,\s?/),
target = obj[key];
delete obj[key];
subkeys.forEach(function(key) { obj[key] = target; })
}
return obj;
}
var holidays = expand({
"thanksgiving day, thanksgiving, t-day": {
someValue : "foo"
}
});
JSON does not offer such a feature, nor do Javascript object literals.
You might be able to make do with something like this:
holidays = {
thanksgiving: {foo: 'foo'},
groundhogDay: {foo: 'bar'},
aliases: {
'thanksgiving day': 'thanksgiving',
't-day': 'thanksgiving',
'Bill Murrays nightmare': 'groundhogDay'
}
}
and then you can check
holidays[name] || holidays[holidays.aliases[name]]
for your data.
It's not a wonderful solution. But it wouldn't be too difficult to write a little function that created this sort of object out of a representation like:
[
{
names: ['thanksgiving', 'thanksgiving day', 't-day'],
obj: {foo: 'foo'}
},
{
names: ['groundhogDay', 'Bill Murrays nightmare'],
obj: {foo: 'bar'}
},
]
if that would be easier to maintain.
Another solution, if you can afford RegExp execution, and ES6 Proxy:
let align = new Proxy({
'start|top|left': -1,
'middle|center': 0,
'end|bottom|right': 1,
}, {
get: function(target, property, receiver) {
for (let k in target)
if (new RegExp(k).test(property))
return target[k]
return null
}
})
align.start // -1
align.top // -1
align.left // -1
align.middle // 0
align.center // 0
align.end // 1
align.bottom // 1
align.right // 1
See MDN Proxy
2021 EDIT:
Another (cleaner?) solution using reduce & defineProperty :
const myDict = [
// list of pairs [value, keys],
// note that a key should appear only once
[-1, ['start', 'left', 'top']],
[0, ['center', 'middle']],
[1, ['end', 'right', 'bottom']],
].reduce((obj, [value, keys]) => {
for (const key of keys) {
Object.defineProperty(obj, key, { value })
}
return obj
}, {})
I guess you could do something like this:
var holidays = {
'thanksgiving day': {
foo: 'foo'
}
};
holidays.thanksgiving = holidays['t-day'] = holidays['thanksgiving day'];
If you see yourself doing this often or you have more values consider this pattern:
'thanksgiving, t-day, thanks, thank, thank u'.split(',').forEach(function(key) {
holidays[key] = holidays['thanksgiving day'];
});
A better approach would be to process your data beforehand instead of adding duplicates.
That should work as expected:
function getItem(_key) {
items = [{
item: 'a',
keys: ['xyz','foo']
},{
item: 'b',
keys: ['xwt','bar']
}];
_filtered = items.filter(function(item) {
return item.keys.indexOf(_key) != -1
}).map(function(item) {
return item.item;
});
return !!_filtered.length ? _filtered[0] : false;
}
With ES6 you could do it like this, but it's not ideal:
const holidays = {
"single": {
singleValue: "foo",
},
...([
"thanksgiving day", "thanksgiving", "t-day",
].reduce((a, v) => ({...a, [v]: {
someValue: "foo",
}}), {})),
"other": {
otherValue: "foo",
},
};
I still think the cleanest solution is probably:
let holidays = {
"t-day": {
someValue: "foo",
},
};
holidays["thanksgiving"] = holidays["t-day"];
holidays["thanksgiving day"] = holidays["t-day"];
Now this may be overkill for you, but here's a generic function that will create an object with "multiple keys." What it actually does is have one real property with the actual value, and then defines getters and setters to forward operations from the virtual keys to the actual property.
function multiKey(keyGroups) {
let obj = {};
let props = {};
for (let keyGroup of keyGroups) {
let masterKey = keyGroup[0];
let prop = {
configurable: true,
enumerable: false,
get() {
return obj[masterKey];
},
set(value) {
obj[masterKey] = value;
}
};
obj[masterKey] = undefined;
for (let i = 1; i < keyGroup.length; ++i) {
if (keyGroup.hasOwnProperty(i)) {
props[keyGroup[i]] = prop;
}
}
}
return Object.defineProperties(obj, props);
}
This is less sketchy than you would expect, has basically no performance penalty once the object is created, and behaves nicely with enumeration (for...in loops) and membership testing (in operator). Here's some example usage:
let test = multiKey([
['north', 'up'],
['south', 'down'],
['east', 'left'],
['west', 'right']
]);
test.north = 42;
test.down = 123;
test.up; // returns 42
test.south; // returns 123
let count = 0;
for (let key in test) {
count += 1;
}
count === 4; // true; only unique (un-linked) properties are looped over
Taken from my Gist, which you may fork.
Same reponse (ES6 Proxy, RegExp), but in a shorter way (and significantly less legible)
let align = new Proxy({
'start|top|left': -1,
'middle|center': 0,
'end|bottom|right': 1,
}, { get: (t, p) => Object.keys(t).reduce((r, v) => r !== undefined ? r : (new RegExp(v).test(p) ? t[v] : undefined), undefined) })
align.start // -1
align.top // -1
align.left // -1
align.middle // 0
align.center // 0
align.end // 1
align.bottom // 1
align.right // 1
//create some objects(!) you want to have aliases for..like tags
var {learn,image,programming} =
["learn", "image", "programming"].map(tag=>({toString:()=>tag }));
//create arbitrary many aliases using a Map
var alias = new Map();
alias.set("photo", image);
alias.set("pic", image);
alias.set("learning", learn);
alias.set("coding", programming);
//best put the original tagNames in here too..
//pretty easy huh?
// returns the image object
alias.get("pic");
// ;)
here is a way you can initialize an object with several keys sharing the same value
var holidays = {
...["thanksgiving day", "thanksgiving", "t-day"].reduce((acc, key) => ({ ...acc, [key]: 'foo' }), {})
}
although I would personally think it was more clear if it was written out
Object.fromEntries produces some fairly readable and concise code:
var holidays = Object.fromEntries(
["thanksgiving day", "thanksgiving", "t-day"].map(k => [k, "foo"]));
The spread syntax can be used to include this alongside other key/value pairs:
var holidaysAndMore = {
"A": "a",
...Object.fromEntries(
["thanksgiving day", "thanksgiving", "t-day"].map(k => [k, "foo"])),
"B": "b"
};

Categories

Resources