Fancy Function Parameters

This article was written by Jake Dohm.

When you're first learning JavaScript one of the first things you learn about is functions, and when you're learning about functions one of the first things you learn about is parameters. But, just because function parameters are so fundamental doesn't mean there isn't room for progress and improvement!

Consider coming across the following line of code in a project that you didn't write, or hadn't worked on in a year.

renderList(['one', 'two'], true, undefined, '#5ad')

Seeing this code, would you have any idea what this function is doing? From a high level perspective, all we can tell is that the function renders a list — but the rest of the arguments passed don't make a lot of sense without reading the function code to figure out what the parameters are, and how the function is using them.

What if I told you there was a better way? In this article, I'll be refactoring the renderList function to make its declaration, and uses, cleaner and more readable.

The Starting Point

The Function

function renderList(list, ordered, color, bgColor) {
    // set defaults for optional parameters
    if(typeof ordered === undefined) ordered = true
    if(typeof color === undefined) color = '#1e2a2d'
    if(typeof bgColor === undefined) color = 'transparent'

    /* code to render list would go here 😉 */
}

Calling the Function

// with all arguments
renderList(['one', 'two'], true, '#c0ffee', '#5ad')

// with only one "optional" argument (bgColor)
renderList(['one', 'two'], undefined, undefined, '#5ad')

In the code above, there are three main problems to address:

  1. When calling the function, it's unclear what the last three arguments are doing. You can tell they're meant to configure how the list appears, but you can't tell how the list will appear without finding and reading the renderList function.

  2. As you can see in the second example of calling the function, a situation may arise where you'll have to pass in undefined for optional parameters. For example, if you want to pass the last argument bgColor without specifying ordered and color, you have to manually pass in undefined. This can be confusing, especially to less experienced developers.

  3. At the top of our function declaration we're having to check if the optional parameters are defined, and assign them default values if they're not. This works just fine, but it's messy, and thanks to ES6 there's an established way to do this.

Naming Our Parameters (Solving Problems #1 & #2)

JavaScript doesn't allow you to name your parameters. This used to be a hard problem to solve, but ES6 introduced a feature which allows us to simulate named params in JS! This powerful feature is called object destructuring. I won't go into depth on how it works, but I recommend taking a further look into destructuring if you're not familiar (here's a fantastic resource on destructuring from Wes Bos: https://wesbos.com/destructuring-objects)

Now, let's name some params!

// original function
function renderList(list, ordered, color, bgColor) { // some things }
renderList(['one', 'two'], true, '#c0ffee', '#5ad')

// with named params
function renderList({ list, ordered, color, bgColor }) { // some things }
renderList({ list: ['one', 'two'], ordered: true, color: '#c0ffee', bgColor: '#5ad' })

Okay, let's break down what we did:

  • See in the second example how there are curly braces just inside of the parentheses?That is destructuring! So while it looks like we're accepting four parameters, it's actually one parameter (an object) that we're destructuring four variables from. The best part is, this doesn't change the way any of our function code works!

  • Then, when we call our function, instead of passing four ordered parameters we pass an object with keys (names) and values.

That wasn't too hard, was it? This simple change makes it much clearer what each argument is doing when we call our function. What you may not have guessed is that changing to this method of accepting params also solved problem #2!

// before
renderList(['one', 'two'], undefined, undefined, '#5ad')

// after
renderList({ list: ['one', 'two'], bgColor: '#5ad' })

Now that we're naming our parameters, and not relying on order, we'll never have to pass in undefined for optional parameters again.

Note: if you ran the "after" example above, the "ordered" and "color" params would be set to undefined because when we destructure the values if no matching key is found the variable is set to undefined.

Naming our parameters solved problems #1 and #2, but it also introduced added complexity. In refactoring our parameters to be named, we've added a slight bit of unnecessary code when we call our function without any of the optional params.

// before
renderList(['one', 'two'])

// after
renderList({ list: ['one', 'two'] })

This definitely isn't the end of the world, but I really like the "before" syntax better than the "after" syntax, and this is a problem we can solve!

By moving "list" out of the object into a separate parameter, we can have the best of both worlds! Our function will now accept two parameters: The list, and an optional settings object which we'll destructure.

// before
function renderList({ list, ordered, color, bgColor }) { // some things }
renderList({ list: ['one', 'two'] })
renderList({ list: ['one', 'two'], ordered: true, color: '#c0ffee' })

// after
function renderList(list, { ordered, color, bgColor } = {}) { // some things }
renderList(['one', 'two'])
renderList(['one', 'two'], { ordered: true, color: '#c0ffee' })

One quick note, before we move on: You may have noticed that in the "after" function declaration there's an = {} after the second parameter (which is destructured). What is this doing? We're setting the "default" value for the second parameter to be an empty object. Why are we doing it? If nothing is passed in as the second parameter, the value of it would be undefined. Now normally this isn't a problem, except that we're attempting to destructure variables from the second parameter, and you obviously can't destructure anything from undefined. In short, we're doing the (almost) equivalent of the following:

const { ordered, color, bgColor } = paramTwo || {};

Setting Defaults for Our Options (Solving Problem #3)

Okay, so we've done a lot **already. The good news is this last part is very simple! Here's a quick refresher on what we're trying to accomplish. We're attempting to clean up the code in our original function that checks to see if our optional parameters are undefined, and sets them to default values if they are. Fortunately, there's a built-in pattern for this too.

Very similarly to how we were able to set a default to a parameter, destructuring also allows us to set defaults for variables we're attempting to destructure from an object. This allows us to remove the semi-complex code that we were using before, and set defaults in a declarative manner.

// before
function renderList(list, { ordered, color, bgColor } = {}) {
    // set defaults for optional parameters
    if(typeof ordered === undefined) ordered = true
    if(typeof color === undefined) color = '#1e2a2d'
    if(typeof bgColor === undefined) color = 'transparent'

    /* ... */
}

// after
function renderList(list, { 
    ordered = true,
    color = '#1e2a2d',
    bgColor = 'transparent'
} = {}) {
    /* ... */
}

This saves us between 1 and 5 lines, depending on if you wrap the options like I did, but the more important improvement here is that you can understand exactly what each parameter's value will be without trying to figure out the "if" statements.

Wrapping Up

With that, we're done! Here is the final refactored code:

The Function

function renderList(list, { 
    ordered = true,
    color = '#1e2a2d',
    bgColor = 'transparent'
} = {}) {
    /* ... */
}

Calling the Function

// simple use
renderList(['love', 'patience', 'pain'])

// with all arguments
renderList(['one', 'two'], { ordered: true, color: '#c0ffee', bgColor: '#5ad' })

// with only one optional argument (bgColor)
renderList(['one', 'two'], { bgColor: '#5ad' })

Hopefully we can all agree that the function is now simpler to use, and it's declaration is much easier to understand! If you have any questions about any of the steps of the refactor, or anything else, check out the articles below for more information or reach out via Twitter (@jakedohm).

Further Reading

Want more? Check out how to set up a Node.js app in VS Code.

The contributors to JavaScript January are passionate engineers, designers and teachers. Emily Freeman is a developer advocate at Kickbox and curates the articles for JavaScript January.