Mocking chainable APIs with ES6 JavaScript Proxies

davidwells

David Wells

Posted on July 23, 2019

Mocking chainable APIs with ES6 JavaScript Proxies

Originally posted on davidwells.io 🎉

ES6 proxies are pretty crazy.

Proxies give you the ability to intercept object calls and do pretty much whatever you want with them 🤯.

I highly recommend checking out this video from Michel Weststrate, the creator of immer, for a super deep dive on ES6 proxies and everything you can do with them.

Beware of proxies he warns, they are cool but can potentially lead to some back debugging issues.

Anywho, onto the use case...

How to mock a chainable API

I came across the need for replacing a chainable API inside the netlify-cli for the chalk module.

We needed a global mechanism for disabling terminal colors made with chalk.

There is a setting for this in chalk but that's the easy road out. We are a developer and we must re-invent the wheel. Also, wasn't working for my version of chalk...

So... Let's try some proxies!

First, what is a chainable API?

// Methods can chain together in any order
chalk.blue.bgRed.bold('Hello world!')

There are lots of libraries out there that allow for this type of flexibility.

You can actually use proxies to create chainable APIs

Proxy time

Thanks to safe-object-proxy, I found a proxy implementation that ensures that objects never throw errors if the keys on that object don't exist.

So no more this:

whatever.haha.yolo()
// Uncaught TypeError: Cannot read property 'yolo' of undefined

Instead, the proxy will magically make the function return null.

Pretty cool

There is a similar project out there called nevernull that comes packed with polyfills if you are running proxies in the browser.

With a little tweaking, console logging, & scratching my head on WTF proxies do, I managed to have the chainable API return my values no matter what.

Success 🎉

// safeChalk.js
const chalk = require('chalk')

/**
 * Chalk instance for CLI
 * @param  {boolean} noColors - disable chalk colors
 * @return {object} - chalk instance or proxy noOp
 */
module.exports = function safeChalk(noColors) {
  // if no colors return chainable "noOp" API
  if (noColors) {
    return NeverNull(chalk)
  }
  // else return normal chalk library
  return chalk
}

/* Chalk NoOp proxy */
function NeverNull(obj) {
  function match(some, none = noOp) {
    return obj != null ? some(obj) : none()
  }
  return new Proxy((some, none) => {
    /* Here was my tweak to make this work with chalks chainable API */
    if (some) return some

    if (!some && !none) return obj
    return match(some, none)
  }, {
    get: (target, key) => {
      const obj = target()
      if (obj !== null && typeof obj === 'object') {
        return NeverNull(obj[key])
      } else {
        return NeverNull()
      }
    },
    set: (target, key, val) => {
      const obj = target()
      if (obj !== null && typeof obj === 'object') {
        obj[key] = val
      }
      return true
    }
  })
}

function noOp() {}

And then using it

const safeChalk = require('./safeChalk')
/**
 * Usage
 */
const disableChalkColors = true
const myChalk = safeChalk(disableChalkColors)

console.log(myChalk.blue.bgRed.bold('Hello world!'))
// 'Hello world!' no coloring

const normalChalk = safeChalk()

console.log(normalChalk.blue.bgRed.bold('Hello world!'))
// 'Hello world!' blue text with red BG that is BOLD

We did it! The chainable API works no matter what!

Wrapping up

As you can see, the proxy is tiny and pretty powerful.

I'd recommend heeding Michael's word of warning with proxies. They are a bit "magical" and debugging proxies looks like a bad time.

What other use cases for proxies have you seen? Leave a comment below

💖 💪 🙅 🚩
davidwells
David Wells

Posted on July 23, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related