Skip to content

Instantly share code, notes, and snippets.

@sthawali
Forked from d-akara/JavaScriptSafeNavigation.md
Created February 17, 2016 09:29
Show Gist options
  • Save sthawali/9a30991070f34a1a83d0 to your computer and use it in GitHub Desktop.
Save sthawali/9a30991070f34a1a83d0 to your computer and use it in GitHub Desktop.
JavaScript Safe Navigation

Experimental Safe JavaScript Navigation

Implemented using ES6 Proxies and Symbols

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'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment