The purpose of this function is to provide a way to avoid deep nested conditionals when traversing a hierarchy of objects. Some languages use an operator such as '?.' to perform this capability. This is sometimes called safe navigation or null conditional operators.
You can somewhat think of this as how a xpath select works. If any nodes along the path are not found, your result is simply not found without throwing an exception and without needing to check each individual node to see if it exists.
Suggestions for improvements welcome!
const nonNavigableTarget = Symbol();
function safe(target, defaultValue) {
// If the target was already wrapped, return the wrapped target
if (target && (typeof target === 'object') && (target[nonNavigableTarget])) return target;
const _this = this;
if ( typeof target === "function") return function() {
return safe(target.apply(_this, arguments), defaultValue);
};
// wrap non object values which we can't futher navigate
if ( typeof target !== "object" || target === null) {
target = target || defaultValue;
target = {[nonNavigableTarget]: {target: target, isResolved: !target || target === defaultValue}};
}
// Create a safe proxy for the target
const proxy = new Proxy(target, {
get: function(target, key) {
// Resolve the actual value when the $ terminator key is used
if (key==='$') {
if (target[nonNavigableTarget]) return target[nonNavigableTarget].target;
return target;
}
// We have already resolved to a non navigable value. Keep returning what we already resolved if there are more lookups
if (target[nonNavigableTarget] && target[nonNavigableTarget].isResolved) return safe(target[nonNavigableTarget].target, defaultValue);
// When a property is requested, wrap it in a proxy
return safe.call(target, target[key], defaultValue);
},
apply: function(target, thisArg, argumentsList) {
// This can only be called on the proxy when there is an attempt to invoke a non function
// function values are wrapped in a function outside of the proxy
return safe(target[nonNavigableTarget].target, defaultValue);
}
});
return proxy;
}
Sample usage of safe navigation
let o = {
name: "User1",
address: {
street: "513"
},
getAddress: function() {
return this.address;
},
getNull: function() {
return null;
},
isNull: null
};
// '.$' signifies the end of the expression and to resolve the value
safe(o).getAddress().street.$ === '513'
safe(o).name.$ === 'User1'
// Example using a default value
safe(o,'name').name.noName.noName2.$ === 'name';
// Example resolving to an object
safe(o).address.$ === o.address
// Example undefined resolutions
safe(o).address.city.country.street.$ === 'undefined'
safe(o).isNull.next.next.$ === 'undefined'
// Example calling a function
safe(o).getNull().street.$ === 'undefined'
// Example calling non existent function
safe(o,'nothing').style().testing.$ === 'nothing'
Would you mind providing a few text paragraphs explaining the goals of this project and the expected input/output. This sounds like a cool microapp, but I don't want to make any false assumptions about what it does versus what it wants to do.