Is destructuring this possible? - javascript

I am new to destructuring and need help finding the best solution. I have a pretty complicated object coming back as a response and would like to clean it up. Without doing anything, it looks like this:
const homeTeam = {
totalPlaysFor: res.data.stats.home.teamStats[0].miscellaneous.offensePlays,
totalPlaysAgainst: res.data.stats.away.teamStats[0].miscellaneous.offensePlays
}
I know I can do something like:
const { offensePlays } = res.data.stats.home.teamStats[0].miscellaneous;
but that only solves my problem for one of them and would leave the other still long and tough to read.

You can destructure the stats property in advance, then make a helper function that takes the .home or .away object and navigates to the nested .offensePlays:
const { stats } = res.data;
const getOffPlays = obj => obj.teamStats[0].miscellaneous.offensePlays;
const homeTeam = {
totalPlaysFor: getOffPlays(stats.home),
totalPlaysAgainst: getOffPlays(stats.away)
};
Without having a standalone helper function, you could also create the object by .mapping an array of properties (eg [['totalPlaysFor', 'home'], ['totalPlaysAgainst', 'away']]) and pass it to Object.fromEntries, but that would be significantly less readable IMO.

While you can destructure this directly, via something like
const {
data: {
stats: {
home: {
teamStats: [{
miscellaneous: {
offensePlays: totalPlaysFor
}
}]
},
away: {
teamStats: [{
miscellaneous: {
offensePlays: totalPlaysAgainst
}
}]
}
}
}
} = res
const homeTeam = {totalPlaysFor, totalPlaysAgainst}
that feels pretty ugly.
Regardless whether you think your code is ugly, I can see a much more important problem with it: it doesn't work when any of those properties doesn't exist. To solve that, you might want a feature not yet ubiquitous in the language, one of Optional Chaining that would avoid throwing errors for a nested path such as 'data?.stats?.home?.teamStats?.0?.miscellaneous?.offensePlays', simply returning undefined if one of the nodes mentioned does not exist.
An equivalent feature is available as a function in many libraries. Underscore's property, lodash's property, and Ramda's path offer slightly different versions of this. (Disclaimer: I'm a Ramda author.) However it's easy enough to write our own:
const getPath = (pathStr) => (obj) =>
pathStr .split ('.')
.reduce ((o, p) => (o || {}) [p], obj)
const res = {data: {stats: {home: {teamStats: [{miscellaneous: {offensePlays: "foo"}}]}, away: {teamStats: [{miscellaneous: {offensePlays: "bar"}}]}}}}
const homeTeam = {
totalPlaysFor: getPath ('data.stats.home.teamStats.0.miscellaneous.offensePlays') (res),
totalPlaysAgainst: getPath ('data.stats.away.teamStats.0.miscellaneous.offensePlays') (res)
}
console .log (homeTeam)
Note that the array is not delineated in this simple version as [0] but just as .0. Obviously we could make this more sophisticated if necessary.

You can destructure as much as you want!
const homeTeam = {
totalPlaysFor: res.data.stats.home.teamStats[0].miscellaneous.offensePlays,
totalPlaysAgainst: res.data.stats.away.teamStats[0].miscellaneous.offensePlays
}
You can even use a combination of array destructuring / object destructuring here:
const [ home ] = res.data.stats.home.teamStats
const [ away ] = res.data.stats.away.teamStats
const { offensePlays: totalPlaysFor } = home.miscellaneous
const { offensePlays: totalPlaysAgainst } = away.miscellaneous
const hometeam = { totalPlaysFor, totalPlaysAgainst }
Or, if you want a more reusable solution, you can use parameter destructuring:
const getTeam = (
{ miscellaneous: { offensePlays: totalPlaysFor } },
{ miscellaneous: { offensePlays: totalPlaysAgainst } }
) => ({
totalPlaysFor,
totalPlaysAgainst
})
Then you can use it like:
const [ home ] = res.data.stats.home.teamStats
const [ away ] = res.data.stats.away.teamStats
const homeTeam = getTeam(home, away)

Related

Can you reference a destructured function parameter in javascript?

I'm going to begin by saying that this is purely a matter of syntax candy-
I have a number of javascript functions with this general signature:
const someFn = async (context, args) => {}
Each implementation deconstructs those two objects. For example:
const myHttpFn = async({req, res}, {id, name, potato}) => { ... }
Within the implementation, I'd like to be able to deconstruct the object in the signature (this makes it easy for users to see what arguments are truly required) but still have a reference to the rest of the object (some properties are pass-through). The closest I've come has been by wrapping both arguments in an object like so:
const { isCold, isDry } = require('./some/utilities');
const lunch({context, context:{fridge, pantry, sandwitchMaker}, {ingredientList, shouldHeat}) => {
const coldList = ingredientList.filter(isCold);
const dryList = ingredientList.filter(isDry);
const [cold, dry] = await Promise.all(
fridge.fetch(context, { list: coldList }),
pantry.fetch(context, { list: dryList })
);
return sandwitchMaker.cook(context, { cold, dry, shouldHeat });
}
const sandwitch = await lunch({context, {
ingredientList: ["bread", "cheese", "tomato", "bacon", "bacon", "lettuce", "mayo"],
shouldHeat: true
}});
Adding the wrapper object gives me access to both context as a whole as well as the pieces that I've deconstructed. Is there a way to do this without a wrapper object?
You can't do it without your so-called wrapper object, but you can have just the wrapper object in the function definition - something like
const lunch({context: {fridge, pantry, sandwitchMaker, ...other}, {ingredientList, shouldHeat}) => {
const context = {fridge, pantry, sandwitchMaker, ...other};
//... rest of your code
}

Spread omit doesn't omit property inside action handler

I feel i'm going crazy, the following code just doesn't work when used in reducer, however running it with the exact same variables in the console or playground works absolutely perfect.
[MutationTypes.DELETE_GOALS_SUCCESS]: (state, { payload }) => {
//payload is {deleted_goals: [1, 2, 3]}, goals is {1: {...}, 2: {...}, ... n: {...}}
const goals = { ...state.goals };
const newGoals = payload.deleted_goals.reduce((acc, id) => {
const { [id]: omitted, ...newAcc } = acc; //newAcc still contains "id" key
console.log(
"After spread",
"New goals:",
newAcc,
"Old goals:",
acc, //acc and newAcc are the same aside from different pointers
"Removed goal",
omitted,
);
return newAcc;
}, goals);
return {
...state,
goals: newGoals,
};
},
The const { [id]: omitted, ...newAcc } = acc; part is what just doesn't work as intended. newAcc for some reason still keeps containing id key, so it remains unchanged every iteration. The id key is included in goals object, i can log omitted object.
As i said i can run the exact same line of code anywhere else with the exact same variables and it will work perfectly. This might be something with redux or my implementation of reducer, however i just cannot imagine what can be wrong and how it can cause such consequences. State is just plain object, state.goal is also just plain object, i'm even making shallow copy of it. I can JSON.stringify them, copy paste somewhere else and then omit the same way i do here and it will work.
Any idea what might cause this strange interaction? There are multiple workarounds to do this without spread, like using delete operator, or constructing new object from scratch but i want to know why the hell can object become "immune" to spread destructuring.
I've tried to omit with spread on fresh object inside both action handler and reduce callback and it worked, looks like there is something with this particular object ( state.goals ). However it is just map like object structured like that: {id1: {goalwithid}, id2:{goalwithid2} ...} id1, id2 etc are numbers.
Just tried deepcloning object (replaced const goals = { ...state.goals }; with const goals = _.cloneDeep(state.goals) and it doesn't change anything.
Why not just do this:
[MutationTypes.DELETE_GOALS_SUCCESS]: (state, { payload }) => {
//payload is {deleted_goals: [1, 2, 3]}, goals is {1: {...}, 2: {...}, ... n: {...}}
const goals = state.goals;
const newGoals = Object.keys(goals).reduce((acc, key) => {
if(payload.deleted_goals.includes(parseInt(key))) {
return acc;
} else {
return (acc[key] = goals[key], acc);
}
}, {})
return {
...state,
goals: newGoals,
};
},

ESLint: Assignment to property of function parameter

Given a structure like:
products: {
'123': {
details: [
{
price: 45,
unitPrice: 0,
productName: 'apples'
}
]
}
}
And a function,
function modifyPriceForApples(coefficient) {
const products = cloneDeep(vehicleProductSelector.getAllProducts(store.getStore().getState()));
let appleProduct;
Object.keys(products).forEach((productId) => {
const {
details
} = vehicleProducts[productId];
details.forEach((detail) => {
const {
price,
productName
} = detail;
if (productName === 'apples') {
detail.unitPrice = coefficient * detail.price;
appleProduct = products[productId];
}
});
});
return appleProduct;
}
I am getting linting error: Assignment to property of function parameter
How can I resolve that, barring disabling the linting rule?
I keep seeing array destructuring as the answer to this problem, however I'm not too sure what that would look like in practice given that this is a pretty complex structure.
Instead of of using forEach, use a map and act as though arguments provided to a function are immutable. It's mad that you are doing details.forEach((detail) => {...}); and then assigning to detail with detail.unitPrice = coefficient * detail.price;.

Can the props in a destructuring assignment be transformed in place?

This works…
const { prop1:val1, prop2:val2 ) = req.query
val1 = val1.toLowerCase()
Though, I'm more inclined to do something like
const { prop1.toLowerCase():val1, prop2:val2 } = req.query
or
const { prop1:val1.toLowerCase(), prop2:val2 } = req.query
neither of which work. Is there a syntax similar to this or must manipulations be done outside of the destructing assignment?
No, this is not possible. A destructuring assignment does only assign, it does not do arbitrary transformations on the value. (Setters are an exception, but they would only complicate this).
I would recommend to write
const { prop1, prop2:val2 ) = req.query;
const val1 = prop1.toLowerCase();
or, in one statement:
const { prop1, prop2:val2 ) = req.query, val1 = prop1.toLowerCase();
The trouble with the temporary variable solutions is that they introduce different versions of the same data into the scope, which can lead to bugs.
This solution creates a utility function that receives the object to be destructured as well as a second object that is a mapping of property names to transformation functions. It's a little more verbose, but does the trick.
// Utility functions to perform specified transformations on an object
function transformProps(obj, trans) {
return Object.assign({}, obj, ...Object.entries(trans).map(([prop, fn]) =>
prop in obj ? {[prop]: fn(obj[prop])} : null
));
}
const { prop1:val1, prop2:val2 } = transformProps(
{prop1: "FOO", prop2: "BAR"},
{prop1: v => v.toLowerCase()} // Transformations to be made
);
console.log(val1, val2);

Parsing structured data in a functional way (e.g. without mutating)

Imagine you have the following data in a file:
Group1
Thing1
Thing2
Group2
Thing1
Thing2
Thing3
Group3
Group4
Thing1
It's easy to write a "parser" which loops through the file line-by-line, remembering the current Group (in a variable) and then writing all the Things to an object, neatly grouped by their respective group:
// Very naive implementation for illustrative purposes only
let groups = {}
let currentGroup = null
data
.split(/\n/)
.forEach(entry => {
const matches = entry.match(/^(Group\d+)$/)
if (matches) {
currentGroup = matches[1]
groups[currentGroup] = []
} else {
groups[currentGroup].push(entry.trim())
}
})
which gives me:
{
Group1: [
'Thing1', 'Thing2'
],
Group2: [
'Thing1', 'Thing2', 'Thing3'
],
...
}
What's the best way to achieve this without mutating groups and currentGroup, in a purely functional way? Do I need to take a harder look at Array.reduce, because I've seen some (IMHO rather mind-boggling) use-cases to transform an Array into an Object, or is that not going to help here?
Yes, you'd want to use reduce here:
data
.split(/\n/)
.reduce(({groups, currentGroup}, entry) => {
const matches = entry.match(/^(Group\d+)$/)
if (matches) {
groups[matches[1]] = []
return {currentGroup: matches[1], groups};
} else {
groups[currentGroup] = groups[currentGroup].concat([entry.trim()]);
return {currentGroup, groups};
}
}, {groups: {}, currentGroup: null})
.groups
However, there is no reasonable way in JS to create a map object without mutation. As long as you keep your property assignments local, there's nothing wrong with that.

Categories

Resources