When accessing nested objects using dot notation, I always have to make sure that the previous object exists, which gets pretty exhausting.
I basically want to avoid long if chains like
if (a && a.b && a.b.c && a.b.c[0] ... ) { v = a.b.c[0]; }
The only other thing I can think of is via the use of a try catch.
var v; try { v = a.b.c[0].d.e; } catch (e) {}
Is there a better pattern for this?
I think you've got the two prettiest solutions already.
But note that for something like, say, obj.obj.string.length your first solution will fail if string === "". Since an empty string is falsey, it'll trip the && guard.
But speaking of strings, you could do something like:
function getNestedProperty(obj, propChain) {
var props = propChain.slice(0), prop = props.shift();
if(typeof obj[prop] !== "undefined") {
if(props.length) {
return getNestedProperty(obj[prop], props);
} else {
return obj[prop];
}
}
}
var v = getNestedProperty(a, ["b", "c", 0, "d", "e"]);
Yeah... not too pretty :P
I'd say that, of the solutions proposed, try...catch is probably the simplest way to go
How about this one:
var hasProperty = function (object, property) {
var properties = property.split('.'),
temp = object;
while (temp && properties.length) {
temp = temp[properties.shift()];
}
return !!temp;
};
and then use it like:
if (a && hasProperty(a, 'b.c.0' ) { v = a.b.c[0]; }
The scenario you are referring to in your question is also called "optional chaining". Some languages already support it by now – for example C# has so called null-conditional operators which allow you to short-circuit your expressions:
var count = customers?[0]?.Orders?.Count();
Unfortunately, this feature has not yet made it into the current JS specifications.
There is an open Stage 1 proposol for "optional chaining" that can be tracked here.
This would allow you to write...
a?.b[3].c?.(x).d
...instead of:
a == null ? undefined : a.b[3].c == null ? undefined : a.b[3].c(x).d
If you want to take the risk and use it already at this early stage, you can target it via babel to include it in your project.
It's rather evil, but this should work and doesn't look too horrible:
var i = !a ? null : !a.b ? null : !a.b.c ? null : !a.b.c.d ? a.b.c.d.e;
The reason for the ! is to invert the test flag, to allow the success case to be the last expression in the ?:. That allows us to chain them together like this.
Do check the operator precedence if you want to do this for real (I did some very basic tests and I think I got it right). And do expect people to point and laugh if they see it in your code.
Related
I have the following chaining operator
const age = data.student?.age ? data.student.age: '';
this works fine in my local machine but seems to have problem in another machine. on further investigation i could understand that node js lower version(i suppose version below 12) doesn't support chaining operator. I understand that i can replace this with if condition like below but would like to know suggestions on what is the best possible alternative for this.
function checkAge(data){
if(data && data.student && data.student.age) {
return data.student.age;
} else {
return '';
}
}
const age = checkAge(data);
There is no need for code change. You only need to modify the target option in your TypeScript configuration and set it to anything ES2019 or below. Then you can use optional chaining in your TypeScript code and the compiler will produce the equivalent code.
The TypeScript code
const foo = a?.b?.c;
becomes
"use strict";
var _a;
const foo = (_a = a === null || a === void 0 ? void 0 : a.b) === null || _a === void 0 ? void 0 : _a.c;
when compiled: Playground Link
If the problem is readability, you could probably try object destructing.
So, your assigment would look something like this:
const {
student: {
age = ''
} = {}
} = data
Assuming declaring age as const, not to polluting scope with intermediate variables, and returning a number even for '0' and empty string in case of undefined are all a must, shorter option that comes to my mind would be following:
const age = (d=>(isNaN(d=(d.student||{}).age)?'':d))(data);
For less strict approaches, cleaner solution would be:
const age = (data.student || {}).age || "";
// In this case, though, numeric 0 would also be returned as empty string.
On the other hand, if you need to do this more than a few times, I would recommend to implement a handy picking function like:
const pick = (target, path)=>path.split(".")
.reduce(
(acc,key)=>acc&&acc[key]
,target
)
;
// And then...
const age = pick(data, 'student.age');
For picking approach it would be worth to reduce the number of
function calls performed by pick() function. I just used reduce for
the sake of brevity and simplicity.
I have an object that I am creating that could potentially have undefined properties.
Is there a more concise way to set the property than what I am doing below:
var ruleObj = {
factor: (ruleArray[2] ? ruleArray[2].trim() : null),
resultElseTarget: (ruleArray[10] ? ruleArray[10].trim() : null)
}
Notice how I have to repeat the variable after the ternary operator twice. The reason I'm asking is that I've run into this same type of problem several times and it doesn't seem like the best way to handle it.
Here’s a function that wraps another function to do nothing on null and undefined values:
const liftMaybe = f => x => x == null ? null : f(x);
Then you can define a trim that does nothing to undefined:
const trimMaybe = liftMaybe(x => x.trim());
and make use of it:
var ruleObj = {
factor: trimMaybe(ruleArray[2]),
resultElseTarget: trimMaybe(ruleArray[10]),
};
It differs from your original in its handling of empty strings, but I don’t know if that was intentional or if it’s even relevant.
Conciseness is one thing but with Javascript the bigger concern is readability and type checking.
In your example, if the value of ruleArray[2] is a boolean then it'd evaluate to false and set factor to null. Maybe that's what you want but just looking at your example code right now, I'd assume your ruleArray contains bools and not potential undefines.
The better way is to write a function to do null check
EDIT: someone was faster than me :) https://stackoverflow.com/a/46436844/643084
EDIT2: the other answer is great but i'd like to just make a note. null should not be treated the same as undefined even though they evaluate the same most of the times.
Some things:
Since you're checking indexes, you'd need to make sure that you have a length of at least the size you want. Otherwise ruleArray[10] can throw you and out of range error.
Assuming you are certain that you have the right number of elements in your array, you can use this to check a var for undefined, this is common to check incoming arguments (say you had something like function ( arg1, arg2 ) ):
arg1 = arg1 || 'some_default';
In your case, again assuming your array is long enough:
factor: ( ruleArray[2] || 'some other default' );
(Why would you set it to null if that's what you are trying to avoid).
If you're wondering, "is there a way to access an index that doesn't exist and just return null", the answer is "maybe...but I wouldn't".
Note, if the value is indeed falsy (say, 0, '', or false), you may not get what you expect, in which case you'd want to check for something more explicit, like null.
I get a lot of use of out the terse "something = thisValIfNotFalsy || someOtherDefaultVal. Like anything though careful when and where, etc.
You could do something like:
var ruleArray = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
function formatRule(rule) {
if (!rule) return null
return rule.trim()
}
var ruleObj = {
factor: formatRule(ruleArray[2]),
resultElseTarget: formatRule(ruleArray[10])
}
console.log(ruleObj.factor)
console.log(ruleObj.resultElseTarget)
We created a pure function that is tasked with producing either null or a trimmed value, which avoids duplicating this logic elsewhere.
Is there a more concise way
So far all answers seem to assume your input is either a string or null / undefined. For me I'd say the check for null / undefined is the wrong way round. You can only call trim on a string, so why not check it's a string?. It would also mean NaN / Numbers / Arrays etc, would not error. I'm assuming what your wanting this function to do is trim a string if it's a string, so I would also say you should pass the original value if not a string.
Maybe that's what #zaftcoAgeiha meant when he talking about not treating null & undefined the same.
Anyway, here is an example. Notice how numbers are still passed, but hello gets trimmed.
const ruleArray = [];
ruleArray[2] = null;
ruleArray[5] = 7;
ruleArray[7] = 'Helllo ';
const trimIfString = (x) => typeof x === 'string' ? x.trim() : x;
var ruleObj = {
factor: trimIfString(ruleArray[2]),
resultElseTarget: trimIfString(ruleArray[5]),
hello: trimIfString(ruleArray[7])
}
console.log(ruleObj);
You can use a function pattern and set default parameter with AND && operator when passing the parameter to check if variable is defined, if not set element value to null. You can include further checks to determine if variable is passed is a string.
let ruleArray = [];
ruleArray[10] = "def ";
let ruleFn = (factor = null, resultElseTarget = null) =>
({factor, resultElseTarget});
let ruleObj = ruleFn(ruleArray[2] && ruleArray[2].trim()
, ruleArray[10] && ruleArray[10].trim());
console.log(ruleObj, ruleObj.factor === null);
This error comes up a lot in javascript development.
cannot read property join of undefined
Is there a best way of dealing with this issue?
Some of the techniques I've used are:
Initialisation
question.tags = question.tags || [];
console.log(question.tags.join(', ');
If statements
if(question.tags) {
console.log(question.tags.join(', ');
}
You can use if..else, Object.hasOwnProperty(), Array.isArray() to determine if question exists and object has property tags and question.tags is an array
if (typeof question === "object"
&& question.hasOwnProperty("tags")
&& Array.isArray(question.tags)) {
//do stuff
} else {
// do other stuff, e.g
// question = {};
// question.tags = [];
}
There is no specific and exact way to do it. If there is an instance of the Array or Object or String, it inhertits the prototypal functions. Like an instance of array has a splice(), String instance has a replace ().
Now when this instance is undefined, it throws JS error. Lets assume a is supposedly an array. You can put a dirty check either by a logical ||
(a || []).length;
or a if block or a ternary property
return a ? a.length || undefined;
or a type check
(Array.isArray(a) || []).length
question.tags ? question.tags : []
You can use === and !== operators in if condition like
if(object.property !== undefined)
{
///write your code here
}
This operator will match your value + type so its easy to identify if mentioned property is undefined or not..hope this will help:)
Is it possible to write this in shorter and cleaner way?
I'm reading it from an XML, sometimes the URL value does not exist.
if (typeof(entry[i].getElementsByTagName("url")[0].childNodes[0]) !== "undefined") {
var foo = 'baar'
} else {
var foo = entry[i].getElementsByTagName("url")[0].childNodes[0]
}
It's been years it doesn't make sense anymore to use this construct (unless you don't know whether the variable, not the value, is undefined). undefined is now read only.
Simply use
if (entry[i].getElementsByTagName("url")[0].childNodes[0] === undefined) {
In almost all cases, typeof x === "undefined" is a bad practice.
In the specific case of a DOM element, you can also simply use
if (!entry[i].getElementsByTagName("url")[0].childNodes[0]) {
because you can't have a falsy node, and of course, when the goal is to apply a default value, just use
var foo = entry[i].getElementsByTagName("url")[0].childNodes[0] || 'baar';
(be careful that this test only works when all the parts before the the last [0] are present, it's usually convenient to use querySelector or a DOM selection API like jQuery to make everything less verbose).
var foo = entry[i].getElementsByTagName("url")[0].childNodes[0] || 'baar'
You can write in this way.
var ele = entry[i].getElementsByTagName("url");
if (ele && ele[0].childNodes[0]) {
var foo = 'baar'
} else {
//code
}
There is no need to check it explicitly for undefined.undefined is evaluated as false.
Say I want to access a.b.c.d and I'm not sure if b or c exist.
The 'naive' check would be:
if (a.b && a.b.c && a.b.c.d == 5) doSomething(a.b.c.d);
I thought this over and wrote this function that improves this:
Object.prototype.parse = function (keys, def) {
return keys.split('.').reduce(function (prev, curr) {
if (prev) {
return prev[curr];
}
}, this) || def;
};
And you would use it like this:
var a = {
b: {
c: {
d: 5
}
}
};
console.log(a.parse('b.c.d', 3)); // If b, c or d are undefined return 3
But I'm wondering if I'm missing a better, native way to achieve this instead of having to add this function to projects.
Thanks!
Maybe not quite what you were asking for, but probably as close to 'native' as you can get (a slightly more compact version of split/reduce snippet you provided):
var a = {b:{c:{d:5}}};
("b.c.d").split(".").reduce(function(p,c){return p && p[c];},a); //5
("b.c.e").split(".").reduce(function(p,c){return p && p[c];},a); //undefined
If you were hoping for a solution with a string like "a.b.c.d", then you'll need to use eval (not recoomended) or the object a will need to be global (also not recommended) or the object will need to be a property of another object which is little self-defeating.
The only native way is eval, but I wouldn't recommend it as can be used to execute arbitrary code. This might be OK, but not if your "a.b.c.d" style strings come from untrusted users. I'd stick with your handcrafted solution or use dotty.
var a = {
b: {
c: {
d: 5
}
}
};
console.log(eval("a.b.c.d"));
eval() will throw the same error that the native equivalent would when b or c are not defined so you'll need to wrap with a try {} catch {} block.
I think I have another solution to this problem, I was trying to achieve this for minutes. If you are working on window scope you can use my function to see if the object exist or return a default value.
function parseObj(obj){
try{
return eval(obj);
}catch(e){
return null;
}
}
Usage
alert(parseObj("a.b.c.d"));
I don't know if this is what you looking for but I am sure it will give you an another idea. Have a nice work.