Skip to content

Instantly share code, notes, and snippets.

@d-akara
Last active April 11, 2024 16:18
Show Gist options
  • Save d-akara/6a87168db66fd8f032d2 to your computer and use it in GitHub Desktop.
Save d-akara/6a87168db66fd8f032d2 to your computer and use it in GitHub Desktop.
JavaScript Safe Navigation

Experimental Safe JavaScript Navigation

Implemented using ES6 Proxies and Symbols

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'
@olivierr91
Copy link

olivierr91 commented May 8, 2018

Function calls do not work if one of the item in the chain before the function call is null or undefined.
Example: safe(undefined).toString().$ results in toString is not a function error. Any idea on how to fix this?

@d-akara
Copy link
Author

d-akara commented Apr 29, 2019

@orobert91 FYI, made a module that includes this functionality.
https://www.npmjs.com/package/safe-objects

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment