BLOG

Create a Null safe world using JavaScript Proxy

1/28/2018Time to read: 5 min

Disclaimer, the content of this post is highly experimental.

I recently learned about the nullable type in Kotlin. If an object might be null, you can append a ? to make access to the attribute safe (that is, not throwing null pointer exception). For example

val name: String? = session?.user?.info?.name

In the above example, we are telling Kotlin compiler: Hey, I know session might be null, however, if it's not null, give me the user. If the user is not null, give me the info...and finally, give me the name. If any of those are null, give me null. I think this is a good approach to The Billion Dollar Mistake. As a runtime error, null pointer exception is totally avoidable by doing a null check before each access. Kotlin adds this simple syntax to help the developer with null checks.

In JavaScript, there are some solutions to do safe drilling into a deeply nested object. For example, we can use lodash/get and do something like _.get(object, 'a[0].b.c');. But I don't like that we have to put path inside a string. First, it's just string, so you can pass in any value. It's up to the utility function to sanitize it. Second, we lost syntax highlighting when we put the path inside a string.

Inspired by this awesome post from Dr. Axel Rauschmayer, I thought it would be fun to use JavaScript Proxy to redirect the access to null/undefined reference, so that null pointer exception won't throw?

To better illustrate, I want the following API:

// given an object below
const pikachu = {
  name: 'pikachu',
  stats: {
    HP: 35,
    attack: 55,
    defend: 50
  },
  say: function() {
    console.log('pika pika');
  }
};

const p = proximize(pikachu); // create a Proxy that wraps pikachu object.
const attack = unproximize(p.stat.attack); // 55
const food = unproximize(p.favorites.food); // undefined
p.speed = 90; // you can set the attribute on Proxy, the original object will be affected.
p.favorites.food = 'ketchup'; // setting food attribute on undefined value has no effect.
p.say(); // "pika pika"
p.sing(); // doesn't have sing function, so no effect.

PS. I found that pikachu likes ketchup from this source.

The main functions are proximize, which wraps any value in a Proxy and unproximize, which unwrap a Proxy and returns a value.

You might be super confused by now. What is Proxy anyway?

Proxy is a new feature in ES2017. It's a very powerful tool to customize some fundamental operations on an object. Those fundamental operations include getting an attribute, setting an attribute, calling the function (if the object is a function), etc. It's a wrapper that intersects certain access to the object.

For example, we can intercept read access to object's attribute:

const hero = {
  name: 'batman'
}

const proxy = new Proxy(hero, {
  get(target, key, receiver) {
    return 'nananana' + target[key];
  }
});

console.log(proxy.name); //nanananabatman

Here we created a Proxy for our hero object and provide a get handler. The get handler intercepts the read access to object's property and customize the return. We can do the same thing for other kinds of operations on the object.

So the idea of using a proxy to prevent null pointer exception is very similar. We proxy the original object so that, When accessing a property, we return a Proxy instead. The returned Proxy has the same structure as the first proxy, but it wraps a different value (the property of the original object). If a proxy wraps undefined or null, accessing any property from the Proxy should return a new Proxy that wraps undefined. This way, we will never have direct access to an undefined/null value.

function proximize(target) {
    return new Proxy({ value: target }, { 
        get(getTarget, key, receiver) {
            if (getTarget.value != null) {
                return proximize(getTarget.value[key]);
            } else {
                return proximize(undefined);
            }
        }
    });
}

The reason why we wrap the target inside an object is in case that the target is not a object. Proxy's constructer requires the first argument to be an object.

Because all the property now returns a Proxy, we need a way to unwrap a proxy to get the value inside. Unfortunately, we don't have a native API to unwrap a Proxy. The closest thing I can think of is to have a unique key. In the get handler, we can do a check:

get(getTarget, key, receiver) {
   if (key === UNIQUE_KEY) {
     return getTarget.value;
   }
  ...
}  

We have to prevent this UNIQUE_KEY to conflict with any possible property on the object so that the proxy won't be accidentally unwrapped when accessing a property.

We can use another ES6 feature for this UNIQUEKEY: [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/GlobalObjects/Symbol).

const UNQIUE_KEY = Symbol();

Every symbol value returned from Symbol() is unique. Let's write the unproximize function.

function unproximize(proxy) {
  return proxy[UNIQUE_KEY];
}

That's it! This is the complete code for get handler:

const UNIQUE_KEY = Symbol();
function proximize(target) {
    return new Proxy({ value: target }, { 
        get(getTarget, key, receiver) {
            if (key === UNIQUE_KEY) {
                return getTarget.value;
            }
            if (getTarget.value != null) {
                return proximize(getTarget.value[key]);
            } else {
                return proximize(undefined);
            }
        }
    });
}

function unproximize(proxy) {
  return proxy[UNIQUE_KEY];
}

// test it out
obj = {hello: 'world', long:{a:{c:4}}}
el = proximize(obj);
console.log(unproximize(el.long)); // {a: {c: 4}}
console.log(unproximize(el.long.a.c)); // 4
console.log(unproximize(el.long.a.c.d)); // undefined

This is just a simple example of using a Proxy to trap get access to object's property. With similar technique, we can also trap set, and apply by providing set handler and apply handler to the proxy. With those two, we can do something like setting a property on undefined or calling undefined as function. Check out my repo with the implementation.

Cool, you may say, but can we use this in production? If you need to support IE 11, then no. Proxy and Symbol are not supported in IE11. Besides that, even though we try to mimic the object, a Proxy is still not the same as the original object. It's dangerous to leak the Proxy out of the scope via returning the Proxy from the function or calling another function with a Proxy as an argument. Without type checking, we are pretty much depending on the variable name to denote that the variable is a proxy instead of an object. The best practice is to do what we need to do with the Proxy, and let it be garbage collected.

Reflection: From School to Work
Wow, I got lucky there
How my messed up code ended up saving me from regression