Destructuring deep properties - javascript
I recently started using ES6's destructuring assignment syntax and started to get familiar with the concept. I was wondering if it's possible to extract a nested property using the same syntax.
For example, let's say I have the following code:
let cagingIt = {
foo: {
bar: 'Nick Cage'
}
};
I know I am able to access extract foo into a variable by doing:
// where foo = { bar: "Nick Cage" }
let { foo } = cagingIt;
However, is it possible to extract a deeply nested property, like bar. Perhaps something like this:
// where bar = "Nick Cage"
let { foo[bar] } = cagingIt;
I've tried finding documentation on the matter but to no avail. Any help would be greatly appreciated. Thank you!
There is a way to handle nested objects and arrays using this syntax. Given the problem described above, a solution would be the following:
let cagingIt = {
foo: {
bar: 'Nick Cage'
}
};
let { foo: {bar: name} } = cagingIt;
console.log(name); // "Nick Cage"
In this example, foo is referring to the property name "foo". Following the colon, we then use bar which refers to the property "bar". Finally, name acts as the variable storing the value.
As for array destructuring, you would handle it like so:
let cagingIt = {
foo: {
bar: 'Nick Cage',
counts: [1, 2, 3]
}
};
let { foo: {counts: [ ct1, ct2, ct3 ]} } = cagingIt;
console.log(ct2); // prints 2
It follows the same concept as the object, just you are able to use array destructuring and store those values as well.
You can destructure a property "as" something else:
const { foo: myFoo } = { foo: 'bar' } // myFoo == 'bar'
Here foo was destructured as myFoo. You can also destructure an object "as" a destructured object
const { foo: { bar } } = { foo: { bar: 'baz' } } // bar == 'baz'
Only one variable was defined in each situation, myFoo and bar, and you can see how they are in similar locations as well, except bar has the { } around it.
You can do this for as many layers of nesting as you like, but if you aren't careful going too many level deep you'll get the old "Cannot read properties of undefined(reading 'foo')".
// Here's an example that doesn't work:
const foo = { bar: { notBaz: 1 } };
const {
bar: {
baz: { // baz is undefined in foo, so by trying to destructure it we're trying to access a property of 'undefined'
qux
}
}
} = foo;
// throws Uncaught TypeError: Cannot read properties of undefined (reading 'baz')
// because baz is 'undefined'
// Won't run due to error above
console.log(qux);
In this case it should be obvious that we shouldn't be trying to destructure it because we can see the definition of foo on the previous line doesn't define the property baz. If the object is coming from an API, though, you aren't always guaranteed that every nested property of your expected result will be non-null or not undefined and you can't know beforehand.
You can set a default value for a destructured object by adding = {}:
const { foo: myFoo = 'bar' } = { baz: 'qux' }; // myFoo == 'bar'
const { bar: { baz } = {} } = { qux: 'quuz' } // baz == undefined
// baz is destructured from the object that was set as the default for foo, which is undefined
// this would throw without the default object, as were trying to destructure from 'undefined'
You can do this for deeply nested destructurings:
// Here's an example that works:
const foo = { bar: { notBaz: 1 } };
const {
bar: {
baz: {
qux // you can default the final item to anything you like as well including null or undefined, but it will be undefined in this case
} = {} // if 'baz' undefined, return {}
} = {} // if 'bar' undefined, return {}
} = foo;
console.log(qux); // logs 'undefined'
If any property is null or undefined along the way, it will cause a cascade of returning empty objects, whose properties to be destructured at the next level will just be undefined. This gets out of hand really quickly though with deeper objects, which can be many lines of code with this formatting. Here's another option that does the same exact thing.
const foo = { bar: { notBaz: 1 } };
const {qux} = foo?.bar?.baz ?? {}; // optional chaining and nullish coalescing
If at any point along the way foo, bar, or baz is null or undefined or null, it will return an empty object that you can destructure( the empty object after ??.
It doesn't make much sense to use destructuring on { qux } if you only need to extract one property, though, because this also requires us to add the nullish coalesced value ?? {}. Below is probably better.
const foo = { bar: { notBaz: 1 } };
const { qux, quux, quuz } = foo?.bar?.baz ?? {}; // this method is better for multiple properties
const quxFromBaz = foo?.bar?.baz?.qux; // this method is better for single properties
For me personally, I think it looks a little messy to include all the optional chaining question marks, but it's better than the alternative with nested destructuring and default values at every level.
And it works with arrays
const foo = {
bar: [
{ a: 1 },
{ b: 2 }
]
}
const c = foo?.bar?.[2]?.c // c == undefined
// bar[2] will be undefined, so trying to access property 'c' would normally throw an error
If you have lodash installed, you can use one of the following:
_.get
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.get(object, 'a[0].b.c');
// => 3
or if you need multiple keys.
_.at
var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };
_.at(object, ['a[0].b.c', 'a[1]']);
// => [3, 4]
You can also safely pair _.at() up with with Array destructuring. Handy for json responses.
[title, artist, release, artwork] = _.at(object, [
'items[0].recording.title',
'items[0].recording.artists[0].name',
'items[0].recording.releases[0].title',
'items[0].recording.releases[0].artwork[0].url'
]);
Three Levels Deep
In case this helps anyone, here's a bit of code that shows how to destructure three levels deep. In this case, I'm using the find() method on an array.
const id = someId
array.find(({ data: { document: { docId }, }, }) => docId == id)
Above, the array data is structured like this (each obj in the array is the same shape):
[{
isSuccess: true,
isLoading: false,
data: {
foo: bar,
...,
document: {
docId: '123',
...
},
}}]
Related
Checking against an object property in an array javascript
var array = []; Now, I am creating an object with post requests. var object = { property 1: req.body.x; property 2: req.body.y }; if the property 1 value(i.e. req.body.x) already exists in any of the objects in the array, it should giving a warning. Else we need to do array.push(object). How to check for the property 1 value every time with the post request.
You can use array.Find in order to check if an object exists in the array with the same property. const arr = [{ prop1: 'foo', // prop1 of `foo` already in array }] // small function so we can avoid code duplication const existsInArray = (obj) => { return arr.find((a) => a.prop1 === obj.prop1) } const obj1 = { prop1: 'foo', } const obj2 = { prop1: 'bar', } if (existsInArray(obj1)) { console.warn('obj1 already in array') } else { arr.push(obj1) } if (existsInArray(obj2)) { console.warn('obj2 already in array') } else { arr.push(obj2) } console.log(arr)
Is there an easy way to access an array element via a string?
With objects, I can wrap a key in square brackets like so: // A.js const category = 'foo' return { [category] : 'bar' } // { foo: 'bar' } Is there an easy way to do the same with array elements? Like // B.js const category = 'foo' const items.foo = [1, 2, 3] const item = 4 return { items: [...items.category, item] } // throws an error I'd like to be able to get {items: [1, 2, 3, 4]} in B.js Is there a way?
Both the dot notation and square brackets are property accessors. If you use the dot notation, the property must be the actual property name: words=new Object; words.greeting='hello'; console.log(words.greeting); //hello console.log(words['greeting']); //hello console.log(words[greeting]); //error In the third example, greeting is treated as a variable, not as a string literal, and because greeting has not been defined as a variable, the JavaScript interpreter throws an error. If we define greeting as a variable: var greeting = 'greeting'; the third example works: words=new Object; words.greeting='hello'; var greeting='greeting'; console.log(words[greeting]); So you need to use square brackets as the property accessor: [...items[category],item]
You can use the same syntax: const category = 'foo' const items = {foo: [1, 2, 3]} const item = 4 console.log({ items: [...items[category], item] })
If you want to access the foo property using another variable, you can just use square brackets notation, like so: const category = 'foo' const items = { foo: [1, 2, 3] } const item = 4 console.log( { items: [...items[category], item] });
Destructuring objects as function parameters deep extend
Is it possible to "deep extend" objects when using destructuring to set default properties of an object passed into a function? Example: function foo({ foo = 'foo', bar = 'bar', baz = { propA: 'propA', propB: 'propB' } } = {}) { console.log(foo); console.log(bar); console.log(baz); } foo({ foo: 'changed', baz: { propA: 'changed' } }); This outputs: (baz is overwrote) changed bar { "propA": "changed" } Is there syntax which will extend the baz object, to give output: changed bar { "propA": "changed", "propB": "propB" }
There is no native-way to do what you ask, but you can write your own function decorator, of course it is a verbose approach... Note: In order to perform a deep merge you should use some of the existing solution developed by the community, both Object.assign and Object Spread Operator perform a shallow copy of the source, even, writing your own deepmerge function could be error-prone. I suggest you to use lodash#merge that is commonly accepted as one of the solid solutions to the problem. var { // https://lodash.com/docs/#merge merge } = require('lodash'); function defaultsArgDecorator(defaults, target) { return function(args) { return target( merge(Object.create(defaults), args) ) } } function _foo(args) { console.log({args}); } var foo = defaultsArgDecorator({ foo: 'foo', bar: 'bar', baz: { propA: 'propA', propB: 'propB' } }, _foo); foo({bar: "ciao bello", baz: {propA: "HELLO"}})
Since parameters destructuring allow you to use previous parameters in the defaults of other parameters, you can create a property that doesn't exit on the original parameters, for example __baz, and set its defaults using baz. In the method you'll use __baz instead of baz. Note: this is a hack, and if the object contains a property by the name of __baz it will override the default, with unexpected results. However, you can name the default property with something like dontUse__baz, which has a very low chance of being used. Default properties using Object#assign: function foo({ foo = 'foo', bar = 'bar', baz, __baz = Object.assign({ "propA": "changed", "propB": "propB" }, baz) } = {}) { console.log(foo); console.log(bar); console.log(__baz); } foo({ foo: 'changed', baz: { propA: 'changed' } }); Default properties using object spread (requires babel plugin - see link): function foo({ foo = 'foo', bar = 'bar', baz, __baz = { "propA": "changed", "propB": "propB", ...baz } } = {}) { console.log(foo); console.log(bar); console.log(__baz); } foo({ foo: 'changed', baz: { propA: 'changed' } });
You can also do nested destructuring. You destructure baz, but also destructure propA and propB. If you need your object whole and don't want to 'rebuild' it, this isn't for you, but it does accomplish one objective of setting default values for unspecified properties nested in an object. I've left baz as a separate descructured value in addition to it's properties as an example of a sometimes handy pattern, but you would most likely want to remove it since it suffers from the same problem of losing default properties. function foo({ foo = 'foo', bar = 'bar', baz, // likely extraneous and confusing, here for example baz: { propA = 'propA', propB = 'propB' } } = {}) { console.log(foo); console.log(bar); console.log(propA, propB); console.log(baz); console.log(Object.assign(baz, {propA, propB})); } foo({ foo: 'changed', baz: { propA: 'changed' } });
Yep, totally possible! Solution 1: function foo({ foo = 'foo', bar = 'bar', baz: { propA = 'propA', propB = 'propB' } = {} }) { console.log(foo) console.log(bar) console.log({propA, propB}) // Because `bar` is renamed to `{propA, propB}`. Hence, no local variable named `baz` } Solution 2 (Workaround): function foo({ foo = 'foo', bar = 'bar', baz = {} }) { // ... const baz2 = Object.assign({}, {propA: 'propA', propB: 'propB'} = {}) console.log(baz2) // Yeah, not really a solution } You can even do this: const func = ([{foo, bar: {a, b: BB, c: CC = 'c'} = {}} = {}], ...{0: rest0 = 0, 1: rest1 = '1'}) => { /* do stuffs */ } The code above is a complicated example, I'll use simpler examples to explain: Example 1: const f1 = ({x: varx}) => console.log(varx) When you call f1(obj), obj.x will be assigned to varx in f1's scope Example 2: const f2 = ({xy: {x, y}}) => console.log({x, y}) Similar to f1 but with {x, y} in place of varx, obj.xy will be assigned to {x, y}, so x = obj.xy.x and y = obj.xy.y Example 3: Now let add create default parameters const f3 = ({xy: {x, y} = {}}) => { /* do stuffs */ } Now, you still have to pass obj as an object but you no longer have to provide obj.xy. If obj.xy is undefined, x and y are undefineds Example 4: Now, let x and y have default values const f4 = ({xy: {x = 'XXX', y = 'YYY'} = {}}) => { /* do stuffs */ } If obj.xy you passed is undefined, {x, y} will be set to {x: undefined, y: undefined}, but since x and y also have default values, x will be 'XXX' and y will be 'YYY' Example 4: "THE ULTIMATE LIFE FORM" const f5 = ({xy: {x = 'XXX', y = 'YYY'} = {}} = {}) => { /* do stuffs */ } Guess I don't have to explain this anymore :D
spread operator issues with property accessors (getters)
I'm having a hard time understanding why there are some issues with the following code https://jsfiddle.net/q4w6e3n3/3/ Note: All examples are tested in chrome version 52.0.2743.116 just in case this helps const model = { someVal: 'some val', }; const obs = { ...model, get accessor() { return this.someVal; }, } // Expected: '>>> some val' // Actual: '>>> undefined' console.log('>>>', obs.accessor); But the following similar snippet works https://jsfiddle.net/q4w6e3n3/5/ const model = { someVal: 'some val', }; const obs = { get accessor() { return this.someVal; }, ...model, } // Expected: '>>> some val' // Actual: '>>> some val' console.log('>>>', obs.accessor); using the babel REPL I was able to see that Object.assign is used if available in the transpiled code When I used it directly instead of the object spread I get the same issue, and also works if put the model variable to the end instead of at the beginning. Is this a bug? or is it intended behavior? Also why does the following snippet work as well?: const model = { someVal: 'some val', }; const obs = { someVal: model.someVal, get accessor() { return this.someVal; }, } // Expected: '>>> some val' // Actual: '>>> some val' console.log('>>>', obs.accessor); https://jsfiddle.net/q4w6e3n3/6/ I would expect it to have the same issues but works as a charm are getters this keyword somehow bound to the object they were added to?
Object.assign won't copy accessors. So when your splat is before your getter babel and its various voodoos uses Object.assign and the accessor isn't copied onto the first object from the second, well kind of, voodoo again. When your splat is after the getter, then it assigns the splatted properties onto the object with the getter, and the getter is preserved. See here: MDN - Object.assign Relevant code excerpt: **Copying Accessors** var obj = { foo: 1, get bar() { return 2; } }; var copy = Object.assign({}, obj); console.log(copy); // { foo: 1, bar: 2 }, the value of copy.bar is obj.bar's getter's return value. // This is an assign function that copies full descriptors function completeAssign(target, ...sources) { sources.forEach(source => { let descriptors = Object.keys(source).reduce((descriptors, key) => { descriptors[key] = Object.getOwnPropertyDescriptor(source, key); return descriptors; }, {}); // by default, Object.assign copies enumerable Symbols too Object.getOwnPropertySymbols(source).forEach(sym => { let descriptor = Object.getOwnPropertyDescriptor(source, sym); if (descriptor.enumerable) { descriptors[sym] = descriptor; } }); Object.defineProperties(target, descriptors); }); return target; } var copy = completeAssign({}, obj); console.log(copy); // { foo:1, get bar() { return 2 } }
What is a concise way to deep test an object hasOwnProperty (or the like)?
I need to do something with an object. Unfortunately the system I'm doing this with (Titanium) expects values to be non-null, otherwise it will segfault. The input, however, cannot be guaranteed to provide sane objects. Here is an example: var data = { foo: { bar: "foobar" } }; function do_it(data) { do_something_with_non_null_value(data.foo.bar); } However it is entirely possible that data is any of the following: var data = null; var data = { foo: null }; var data = { foo: { bar: null } }; How can I test for a non-null value in a deep but concise manor to prevent the do_something_with_non_null_value() from crashing? Underscore.js answers are also welcome.
How about using defaults method from underscore? function do_it(data) { var defaults = { foo: { bar: null } }; data = _.defaults(data || {}, defaults); do_something_with_non_null_value(data.foo.bar); } If data is any of this: var data = null; var data = { foo: null }; Will change the object to: var data = { foo: { bar: null } };