DEV Community

Cover image for The Neverending Object
Philip Birk-Jensen
Philip Birk-Jensen

Posted on • Originally published at blog.birk-jensen.dk on

The Neverending Object

I've been messing with the Proxy object lately, and I've created a very simple proxy, that allows for a neverending object chain. It's basically a chain of No operations, and yes I know it's a hard sell, but I used it to rekindle one of my old projects and it can work a (tiny) bit like Optional chaining (?.), so I thought I'd share.

Implementation

It's a small proxy, with a [[ProxyHandler]] that traps [[Get]] and [[Call]], it simply returns the proxy object itself in each trap:

const neverendingObject = new Proxy(function () { }, {
  apply: function () { return neverendingObject; },
  get: function () { return neverendingObject; }
});
Enter fullscreen mode Exit fullscreen mode

Note the [[ProxyTarget]] is a function, this will allow a neverendingObject() function call without any extra chaining.

For educational purposes, I'll add logging in the traps, this way it's easier to get what's happening:

const neverendingObject = new Proxy(function () { }, {
  apply: function (target) { console.log('[[Call]] ()'); return neverendingObject; },
  get: function (target, prop) { console.log('[[Get]]', prop); return neverendingObject; }
});

neverendingObject.aProperty.aFunction().aProperty

// [[Get]] aProperty
// [[Get]] aFunction
// [[Call]] ()
// [[Get]] aProperty
Enter fullscreen mode Exit fullscreen mode

How it works

I had some trouble wrapping my head around the apply part, the extra get call getting the reference to aFunction first and then calling it after.

I've included the call chain below highlighting each step of the way, thinking of it that way helped me out (remember that each [[Get]] and [[Call]] will return the neverendingObject object).

  1. neverendingObject [[Get]] aProperty

  2. neverendingObject.aProperty [[Get]] aFunction

  3. neverendingObject.aProperty.aFunction [[Call]] ()

  4. neverendingObject.aProperty.aFunction() [[Get]] aProperty

  5. neverendingObject.aProperty.aFunction().aProperty

Hopefully, this didn't add to any confusion.

The optional chaining (?.) case

The neverendingObject can not replace the ?. operator, but it's possible to mimic the behavior of chaining without testing every step. but only if the last action is a function, and yes that's a show-stopper for most use cases.

const person = {
  arms(hasNoArms) {
    if (hasNoArms === true) {
      return neverendingObject
    } 

    return {
      wave() { console.log('Waving arms'); }
    };
  }
}

person.arms().wave(); // "Waving arms"
person.arms(true).wave(); // Nothing happens
Enter fullscreen mode Exit fullscreen mode

Using the ?. operator, it would look like the following (assuming null is returned instead of the neverendingObject, when the hasNoArms parameter is set):

person.arms(true)?.wave(); // Nothing happens
Enter fullscreen mode Exit fullscreen mode

The ?. operator is more explicit and would be the preferred way to go most of the time. As a bonus the ?. operator stops execution of the rest of the chain, where the neverendingObject will need to run the chain to its end, including any expensive calculation as an argument.

An actual use case

With everything, there's wrong about the neverendingObject, I found a use case where it shines, which is adding conditional chaining to existing objects.

A very simple case is adding an and function to the console object, giving an easy way to only log output if a condition is true without the added if:

console.and = function (condition) {
  if (condition) {
    return console;
  }
  return neverendingObject;
}

console.and(1 == 1).log('is logged'); // "is logged"
console.and(1 == 2).log('is logged'); // Nothing happens
Enter fullscreen mode Exit fullscreen mode

Conclusion

Let's compare a simple if with the and() function:

if (isDebugging) {
  console.log('Some message');
}

// vs

console.and(isDebugging).log('some message');
Enter fullscreen mode Exit fullscreen mode

It saves a couple of lines, and it's (subjectively) easier to read. When using the time() and timeEnd() to do some light profiling those couple of lines add up:

if (isDebugging) {
  console.time('abcd');
}
// Expensive stuff
if (isDebugging) {
  console.timeEnd('abcd');
}

// vs

console.and(isDebugging).time('abcd');
// Expensive stuff
console.and(isDebugging).timeEnd('abcd');
Enter fullscreen mode Exit fullscreen mode

I've started a new branch of an old project: ConditionalConsole.

The previous version needed to create another object than console. Using this technique I'm able to decorate it and make the functionality have a more native feel.

Top comments (0)