How to get or set a JavaScript Map without repeating myself - javascript

I have a use case where I want to get an item from a Map. If that item doesn’t exist, I want to insert an initial value and save it for later. I also would like to use const variables so that I can’t accidentally rebind the local. I know I can write my own helper, but it seems to me that this pattern requires a lot of writing for a pattern which I expect to be quite common:
const item = myMap.has(key) ? myMap.get(key) : myMap.set(key, initialize()).get(key);
or to avoid the .get() immediately after the .set():
const item = myMap.has(key) ? myMap.get(key) : (() => {const value = initialize(); myMap.set(key, value); return value})();
Is there a simpler way to get an item from a Map into a const variable and, if it doesn’t yet exist, insert an initial value for the key first?
I know that a similar question exists, but in that question there is no requirement that the inserted item be stored in a local after insertion and it uses let rather than const.
EDIT: I’ve gone with the utility function route and created maputil.

We can reduce it ever so slightly, factoring out the .get(key) part:
const item = (myMap.has(key) ? myMap : myMap.set(key, initialize())).get(key);
...but I don't think we can go further without introducing a new function.
const initialize = _ => 42;
const myMap = new Map();
const key = "answer";
const item = (myMap.has(key) ? myMap : myMap.set(key, initialize())).get(key);
console.log(item);
console.log(Array.from(myMap.keys()));
So yeah, probably a candidate for a utility function. :-)
Standalone:
const getOrAdd = (map, key, initializer) => {
if (map.has(key)) {
return map.get(key);
}
const value = initializer();
map.set(key, value);
return value;
};
const initialize = _ => 42;
const myMap = new Map();
const key = "answer";
const item = getOrAdd(myMap, key, initialize);
console.log(item);
console.log(Array.from(myMap.keys()));
...or if you can extend builtins in your project (it's okay for some projects, not for others):
Object.defineProperty(Map.prototype, "getOrAdd", {
value: function(key, initializer) {
if (this.has(key)) {
return this.get(key);
}
const value = initializer();
this.set(key, value);
return value;
}
});
const initialize = _ => 42;
const myMap = new Map();
const key = "answer";
const item = myMap.getOrAdd(key, initialize);
console.log(item);
console.log(Array.from(myMap.keys()));

Related

Set arguments dynamically with Promise.all().then()

The code below works for me
Promise.all([first, second, third]).then([first, second, third] => {
console.log(second);
});
I know that console.log(second) will give me the value with the key second.
My promises are dynamically set and now it looks like below:
let collection = [second, third];
Promise.all(collection).then((collection) => {
console.log(collection);
});
In this example I set two values in collection. In real life it can include more or less values.
When I use console.log(collection) it will output collection[0] and collection[1]. In this case I don't know what which value collection[1] is.
Question
How can I, like my first example, have something like named dynamically arguments like collection['second'] or similar?
As we want to access the value dynamically, set collection to an empty object first. Then, use the keys from collection to pass all its Promise-values to Promise.all. Then, map back the fulfilled values and then, we can access collection's value by some key.
let collection = {}
for (let i = 0; i < 3; i++) {
collection[`key${i}`] = Promise.resolve(i)
}
let collectionKeys = Object.keys(collection)
Promise.all(collectionKeys.map(key => collection[key]))
.then(values => {
let collectionFulfilled = collectionKeys.reduce((obj, key, i) => {
obj[key] = values[i]
return obj
}, {})
console.log(collectionFulfilled)
})
If you pass your promises embedded inside an object with a single key, you could use that for it's name, and then with a simple helper function reverse the values & keys from this.
With the new ES6 you can then just pass like -> [{one}, {two}, {three}] etc.
Below is an example with a helper function called namedPromiseAll.
function namedPromiseAll(named) {
const pcollection =
named.map(m => Object.values(m)[0]);
const ncollection =
named.map(m => Object.keys(m)[0]);
return Promise.all(pcollection).then((c) => {
return c.reduce((a,v,ix) => {
a[ncollection[ix]] = v;
return a;
}, {});
});
}
const second = Promise.resolve(2);
const third = Promise.resolve(3);
const collection = [{second}, {third}];
namedPromiseAll(collection).then(console.log);

Angular/TypeScript Map using last index on every object

I'm trying to create an array of 120 objects (hex's for a map) and then add info to each one.
When I use map the id is supposed to be the index of the current object and I can console log the correct, current index as expected but for some reason EVERY object has an id of 119. I've looked at some of the other map examples on here and with Mozilla and I'm not understanding where I'm going wrong.
export class MapComponent implements OnInit {
arrayOfObjects = Array(120).fill({})
hexTiles = this.arrayOfObjects.map( (hex, index) => this.createHex(hex, index))
constructor() { }
ngOnInit() {
}
createHex(hex, index){
console.log('Current Index', index)
hex['id'] = index
hex['x-coordinate'] = null
hex['y-coordinate'] = null
hex['mapped'] = false
hex['terrain'] = ''
hex['details'] = ''
return hex
}
}
UPDATE
I have tried a suggested solution but am now getting an array of 120 empty objects.
Here is the updated code:
HTML: This displays nothing.
<div>
<div *ngFor="let hex of hexTiles; let i = index">
<pre>
{{hex}}
</pre>
</div>
</div>
TS: This will console log "Hexs (120) [empty × 120]"
export class MapComponent implements OnInit {
hexTiles = [...Array(120)].map((_, i) => this.createHex({}, i));
constructor() { }
ngOnInit() {
console.log('Hexs', this.hexTiles);
}
createHex(hex, index) {
hex['id'] = index;
hex['x-coordinate'] = null;
hex['y-coordinate'] = null;
hex['mapped'] = false;
hex['terrain'] = '';
hex['details'] = '';
return hex;
}
}
Change
arrayOfObjects = Array(120).fill({})
hexTiles = this.arrayOfObjects.map( (hex, index) => this.createHex(hex, index))
for this
hexTiles = [...Array(120)].map((_, i) => this.createHex({}, i));
Here is a working snippet:
function createHex(hex, index) {
hex['id'] = index;
hex['x-coordinate'] = null;
hex['y-coordinate'] = null;
hex['mapped'] = false;
hex['terrain'] = '';
hex['details'] = '';
return hex;
}
const hexTiles = [...Array(120)].map((_, i) => createHex({}, i));
console.log(hexTiles);
Here is an explanation of your problem.
function createHex(hex, index) {
hex['id'] = index;
hex['x-coordinate'] = null;
hex['y-coordinate'] = null;
hex['mapped'] = false;
hex['terrain'] = '';
hex['details'] = '';
return hex;
}
const obj = { };
const arrayOfObjects = Array(120).fill(obj);
const hexTilesBad = arrayOfObjects.map((hex, index) => createHex(hex, index));
console.log(obj)
When filling the array with {}, you are actually filling it with copies of the reference to the same object. This is because, by default, in Javascript/Typescript, when you assign an object, you assign its reference.
In the above example, instead of filling the array with {}, we fill it with obj, which is just {}. When we console.log(obj) at the end, we see that it indeed has the value of the last iteration.
Its reference has been copied 120 times in the array, and those references got passed to the map, then to your createHex function. So ultimately, you are just modifying 120 times the same object.
However, when you pass {} directly to createHex from the map arrow function, a new object is created for each iteration in the function scope, so nothing is shared.
EDIT:
[...Array(120)] does not seem to work in TypeScript. As pointed out by #AJT_82 in the comments below, this is due to a TypeScript bug which causes this spread operator trick to incorrectly translate to JavaScript.
To circumvent the problem, you can try the following:
Array.from({ length: 120 })
or stick with your method
Array(120).fill({})
What is important is to not use the content of this array, since they are just references to the same object.

Check if key exist

I have groupedTags and I need to add fields to it and add new tags across field.:
let groupedTags = {
'other': {}
}
if (!groupedTags.other[field]) {
groupedTags.other[field] = [];
}
groupedTags.other[field].push(tag);
I understand that it is necessary to initialize a new field to push new tag - Is there a more beautiful way to check if a field exists every time? I mean avoid explicit check. or is there nothing terrible about this check? there are just a lot of places where it can be repeated
Maybe you should investigate using Proxies for achieving your desired result.
Here's short example of doing so CodeSandbox -example
1) Create proxy handler to customise Object behaviour
const handler = {
set: function(obj, key, value) {
if (!obj[key]) {
obj[key] = [];
}
obj[key].push(value);
return true;
}
};
2) Assign Proxy to your Object
let groupedTags = {
other: new Proxy({}, handler)
};
Now assigning new value will go trough Proxy
groupedTags.other.b = "bar";
// {"other":{"b":["bar"]}}
If you want to create an array of elements on an empty (or not) object, you can try this. So you don't have to check if the property you want to push your element(s) already exists.
If it doesn't it will concat with an empty array, giving you an array with the one element, otherwise the value will be added to that array. Hope this helps.
const o = {};
const toAdd = [1,2,3,4];
toAdd.forEach((v) => {
o.other = (o.other || []).concat(v);
});
o.other2 = (o.other2 || []).concat(123);
console.log(o);
I believe there is no way around checking for a value + if needed initalizing it, other than doing it yourself explicitly.
You can take out one occurrence of groupedTags.other[field] using || like this:
let field = "foo", tag = "bar";
let groupedTags = {
'other': {}
}
// Get the existing items, or assign a new list & use it:
var items = groupedTags.other[field] || (groupedTags.other[field] = []);
items.push(tag);
console.log(groupedTags);
You could also make use of a helper method that encapsulates the check-and-init part:
let groupedTags = {
'other': {}
}
AddTag(groupedTags.other, "foo1", "bar1");
AddTag(groupedTags.other, "foo2", "bar2a");
AddTag(groupedTags.other, "foo2", "bar2b");
console.log(groupedTags);
// Can put this in a library.js
function AddTag(obj, field, tag) {
var items = obj[field] || (obj[field] = []);
items.push(tag);
}

Remove an object's key and value using a variable from function

Hey I'm trying to remove a key:value pair from state inside a Javascript Object.
It works when I hardcode the key name in the code, but when I try to use a variable from a function call, it does nothing.
Can somebody help me out?
Here's an object example:
toppingsSelected: {
"Onion":"true",
"Mushrooms":"true",
}
This works, hardcoded:
deleteTopping = toppingName => {
const { Onion, ...withoutOnion } = toppingsSelected;
console.log(withoutOnion); // Returns object without onion
};
This doesn't work:
deleteTopping = toppingName => {
const toppingName = "Onion"; // Variable gets passed in
const { toppingName, ...withoutOnion } = toppingsSelected;
console.log(withoutOnion); // Returns original object, no change made
};
So I'm basically trying to remove a key from React state but I'm pretty new to Javascript.
How can I make Javascript aware that toppingName is a key?
Another option is to add square brackets arround toppingName, and assign it to a variable. As #Bergi pointed out in the comments, this option does not mutate toppingsSelected
const toppingsSelected = {
"Onion":"true",
"Mushrooms":"true",
};
const toppingName = "Onion";
const {
[toppingName]: topping,
...withoutOnion
} = toppingsSelected;
console.log(JSON.stringify(withoutOnion));
To set the React state, you'd then do this
this.setState({ toppingsSelected: withoutOnion })
You can use delete e.g.
delete toppingsSelected[toppingName];
One way of doing this is using Array.prototype.filter()
const _obj = {
'Onion': true,
'notOnion': false
};
const newObj = Object.keys(_obj)
.filter(key => key !== 'Onion')
.reduce((acc, cur) => ({ ...acc, cur }), {})
console.log(newObj); // { notOnion: false }
This will return a new object without the 'Onion' property

js map why is not changing my value with return

I'm doing map with a variable, and doing a return to change his value, but it's not working, the value for the whole array is the same:
//resultValues = processValues(table,resultValues,'toClient');
resultValues.map ( (record) => {
record = processValues(table,record,'toClient');
return record;
});
return Promise.resolve(resultValues); // does not change
so I had to create another variable to be able to have a change on the array. why is this behavoiur? it's normal in map ?; is there another option with lodash by example to don't need create a second variable?
let newResult = [];
resultValues.map ( (record) => {
record = processValues(table,record,'toClient');
newResult.push(record); // with this changes are sent to new var
//return record;
});
// return Promise.resolve(resultValues);
return Promise.resolve(newResult);
Array.map returns a new array instance where each element inside it is transformed:
let ret = resultValues.map (record => processValues(table,record,'toClient'));
return Promise.resolve(ret);
Map returns a new array. You could literally do:
const newArr = resultValues.map ( (record) => {
record = processValues(table,record,'toClient');
return record;
});
Please read the first sentence of the MDN web docs for map().

Categories

Resources