Object property that can be getter or function

Viewed 68

I am coding a chainable library and I want the API to allow calling part of the chain as static values (with a default value) and sometimes as functions, so parameters could be pass to them.

Simplified example:

var obj = {};
var chainCache = [];

Reflect.defineProperty(obj, 'color', {
  get(){
    chainCache.push('red');
    return obj;
  }
})

Reflect.defineProperty(obj, 'background', {
  get(){
    chainCache.push('black');
    return obj;
  }
})

Reflect.defineProperty(obj, 'end', {
  value(){
    var value = chainCache.join(" ");
    chainCache.length = 0;
    return value;
  }
})

console.log( obj.color.background.end() ) // red black

This is a very simplified example and in reality I would also like to include an ability in the above "API" to optionally use the same color key, like this:

obj.color.background.end()          // current API (great)
obj.color('#FF0').background.end()  // optionally call "color" as function
obj.color().background.end()        // bad API, I do not want this

Can color be both function and property at the same time, depending how it is called?

2 Answers

I took a harder look at this than I intended. It looks like with the advent of Proxies this is possible. Building on your example:

let chainCache = [];
let obj = {
    color: new Proxy(()=>{}, {
        get: (target, prop) => {
            chainCache.push("red");
            if(prop === "background") {
                chainCache.push("black");
                return { end: obj.end };
            }
        },
        apply: (target, prop, args) => {
            if(args.length > 0) {
                chainCache.push(args.pop());
            } else {
                throw new Error("color expects argument if called like function");
            }
            return { background: new Proxy(()=>{}, {
                get: (target,prop) => { chainCache.push("black"); return obj.end;}
            })};
        }
    }),
    end: () => {
        let value = chainCache.join(" ");
        chainCache = [];
        return value;
    }
};
console.log(obj.color.background.end());
console.log(obj.color("#FF0").background.end());
console.log(obj.color().background.end());

In the debug console you get:

red black
#FF0 black
Error: color expects argument if called like function

Essentially color is a proxy that wraps an anonymous arrow function, if color is accessed like a property the get() trap gets called, if color gets accessed as a function the apply() trap gets called. This allows for a certain degree of meta-programming like you're looking for.

You will need to make obj a function.

A function is just an object and can have properties as well - obj.color must return a function (for obj.color()) that has properties (for obj.color.background).

But no, when the getter is accessed you don't know yet whether it will be used for a method invocation or not - obj.color() is a property access plus a function call.

var chainCache = [];
var obj = Object.defineProperties(function obj(...args) {
  chainCache.push(args);
  return obj;
}, {
  color: {
    get() {
      chainCache.push('color');
      return obj;
    }
  },
  background: {
    get() {
      chainCache.push('background');
      return obj;
    }
  },
  end: {
    value: function() {
      var res = chainCache.reduce((acc, val) =>
        Array.isArray(val)
          ? `${acc}(${val.join(',')})`
          : `${acc}.${val}`
      , "obj");
      chainCache.length = 0;
      return res;
    }
  }
})
Related