loading...
Cover image for Maintainable Options Parameters for Functions in JavaScript

Maintainable Options Parameters for Functions in JavaScript

davejs profile image David Leger Originally published at Medium ・3 min read

My friend/co-worker Sam Jesso told me that he hates using flags to modify the behaviour of functions. It makes sense. Functions should follow the single responsibility principle and do exactly one thing. It makes testing and maintaining code easier because it keeps functions simple and concise. However, diving into almost any codebase will reveal that we often make exceptions and use flags.

Whether or not these exceptions are justified is not what I want to discuss. It would be impossible to come up with a set of rules or guidelines for when an exception makes sense because everybody’s code is different. But if you’ve already decided that you want to pass flags into your function, there is a simple trick you can use to make your functions’ interfaces more developer-friendly.

Rather than treating flags (or options) as separate parameters, we can group them into a single options object:

// Before
function func(inputA, inputB, flagA, flagB, flagC) { /* ... */ }
func('Some input.', 'Some more input.', true, false, true);

// After
function func(inputA, inputB, { flagA, flagB, flagC } = {}) { /* ... */ }
func('Some input.', 'Some more input.', { flagA: true, flagC: true });

Grouping options into a single object has several advantages over using separate parameters. To better understand these advantages, let’s take a look at a less abstract example...


An Example with Formatting Time

Here’s a simple function to get a formatted time string from a Date object:

function formatTime(dateTime) {
  const hours   = leftPad(dateTime.getHours(), 2);
  const minutes = leftPad(dateTime.getMinutes(), 2);
  const seconds = leftPad(dateTime.getSeconds(), 2);

  return `${hours}:${minutes}:${seconds}`;
}

formatTime(new Date());  // 01:23:45

Side note: Yes, I wrote my own version of leftPad because I’m not pulling in simple dependencies for a blog post. (Also if you don’t cringe when you hear talk of leftPad, take a moment to read this.)

function leftPad(number, numberOfChars) {
  let paddedNumber = `${number}`;
  numberOfChars -= paddedNumber.length;
  while (numberOfChars--) paddedNumber = `0${paddedNumber}`;
  return paddedNumber;
}

And yes, I know how error-prone this is but it works for our implementation here.

Anyway, back to the example.

New Requirements

We have a function for formatting time and it does a great job. But now we want to have the option switch between 12-hour and 24-hour time. And we also want to exclude seconds in some cases.

No problem, we can just add some extra parameters to the function:

function formatTime(dateTime, is12Hours, showSeconds = true) {
  const hours   = leftPad(is12Hours ? dateTime.getHours() % 12 : dateTime.getHours(), 2);
  const minutes = leftPad(dateTime.getMinutes(), 2);
  const seconds = showSeconds ? `:${leftPad(dateTime.getSeconds(), 2)}` : '';

  return `${hours}:${minutes}${seconds}`;
}

formatTime(new Date(), true, false);  // 01:23

There are several issues with this approach:

  • The parameters must be passed in a specific order. If we want to hide seconds, we must still pass a value for is12Hours before we can specify one for showSeconds.
  • The parameters are unnamed. If the function is being called far away from the definition, it may not be clear what the parameters mean. We must go to the function definition to find out what the various true/false values do.

These issues make the function interface very difficult to read understand and they amplify the potential for human error especially when a function has many options because it’s easy to accidentally skip a parameter or mix up their order.

Refactoring with an options object

A simple way to fix these issues is to refactor the function to use an object for flags/options:

function formatTime(dateTime, { is12Hours, showSeconds = true } = {}) {
  const hours   = leftPad(is12Hours ? dateTime.getHours() % 12 : dateTime.getHours(), 2);
  const minutes = leftPad(dateTime.getMinutes(), 2);
  const seconds = showSeconds ? `:${leftPad(dateTime.getSeconds(), 2)}` : '';

  return `${leftPad(hours)}:${leftPad(minutes, 2)}${seconds}`;
}

const time = formatTime(new Date(), { 
  is12Hours: true, 
  showSeconds: false 
});   // 01:23

This approach solves the issues that exist with passing flags as separate parameters by:

  • Exposing the flag names to the interface.
  • Forcing developers to label the flags properly.
  • Making the ordering of flags irrelevant.
  • Allowing exclusion of flags when we want the default behaviour.

In addition to making the function more readable we’ve also made it maintainable because it’s now easier to add many flags to our formatTime function without adding more and more nameless booleans, making the function calls unreadable. We could add flags for showMinutes, showMilliseconds, or even an option to specify a custom delimiter to replace the default colon. Whatever flags or options we add, the function will remain relatively readable.


One More Thing...

Even though we’ve made the function’s interface easy to use and add to doesn’t mean that all the functionality for those parameters should be aggregated into a single function. Use your best judgement and decide when to delegate functionality to helper functions.

Posted on by:

davejs profile

David Leger

@davejs

Frontend developer passionate about creating things that help people live happier lives.

Discussion

pic
Editor guide
 

This is a great syntaxe I sometimes try to abide.
Little drawbacks for this is that your function signature can go crazy if you have a lot of options. And the default values are static (which is mostly a good thing, but can be a pain in other situations).

You can use Object.assign, but it lost the "Exposing the flag names to the interface".