jchi2241
jchi2241

Reputation: 2226

Wrapping a function that has optional, positional arguments

I want to write a wrapper around Segment's page method, where it'll take the called arguments and merge my set of options in with the options passed, and pass the resulting options along with the other arguments into page and calling it.

A naive way of writing this is:

analytics.page = () => {
  const options = {
    ...arguments[3],
    "mydefault": "options",
  }
  analytics.page(arguments[0], arguments[1], arguments[2], options, arguments[4]);
}

The issue is all of page's arguments are positional and optional. In addition, the arguments are treated differently depending on the number of them you pass in. i.e., if only one argument is provided, the argument is treated as a name rather than a category.

analytics.page("im a category", "im a name");
analytics.page("im a name now");

Is there a clean way to deal with these cases, or will I need to write a bunch of conditionals checking for the length of the arguments object - essentially replicating how page internally parses the arguments?

Upvotes: 0

Views: 169

Answers (2)

t.niese
t.niese

Reputation: 40842

First of all, there are two problems in your code in general:

  1. can't use arguments top get the arguments passed to an arrow function, so you have to use a regular function for that. Or define the arguments yourself using e.g. ...args.

  2. analytics.page(arguments[0], arguments[1], arguments[2], options, arguments[4]); would great an infinite recursion, because it would call your arrow function and not the original one.

So you need to save the original page function and forward the changed arguments list using e.g. MDN Function.prototype.apply(), if you need to apply or not depends on if the original page function uses this to refer to analytics or not.

let analytics = {
   page() {
        console.dir(arguments)
   }
}

let orgPage = analytics.page
analytics.page = function() {

  // in case that the fourth parameter with your option is optional.
  if( arguments.length > 3 ) {
    const options = {
      ...arguments[3],
      "mydefault": "options",
    }

    arguments[3] = options;
  }

  return orgPage.apply(analytics, arguments)
}

analytics.page(1,2, 3,{foo: 1, bar: 1}, 5)

Upvotes: 0

CertainPerformance
CertainPerformance

Reputation: 370699

You have to identify which argument passed, if any, is the options. Luckily, this is isn't that hard: if the options argument is passed, it will be an object, not a function, and will come after another object. Iterate over the arguments, starting from the first. If a non-function object is found, set a flag. If another non-function object is found, that's the options - change it as needed. Otherwise, you need to insert the default options at the right position, which can be done by checking whether the last argument is a function or not:

const changeOptions = (...args) => {
  const defaultOptions = { "mydefault": "options" };
  let propertiesArgFound = false;
  for (const [index, arg] of args.entries()) {
    if (typeof arg === 'object') {
      if (propertiesArgFound) {
        // This is the options object, mutate it:
        args[index] = { ...defaultOptions, ...arg };
        return args;
      } else {
        propertiesArgFound = true;
      }
    }
  }
  // Options object was not found, add it to the argument list
  // either at the last position, or at the next-to-last position:
  if (typeof args[args.length - 1] === 'function') {
    args.splice(args.length - 1, 0, defaultOptions)
  } else {
    args.push(defaultOptions);
  }
  return args;
};
const analyticsPage = (...args) => {
  const newArgs = changeOptions(...args);
  // analytics.page(newArgs);
  console.log(newArgs);
};

analyticsPage('foo', 'bar', { property: 'baz' });
analyticsPage('foo', 'bar', { property: 'baz' }, { someOption: 'option' });
analyticsPage('foo', 'bar', () => 'callback');

Upvotes: 1

Related Questions