I wrote my first few lines of JavaScript not long after the language was invented. If you told me at the time that I would one day be writing a series of articles about elegant patterns in JavaScript, I would have laughed you out of the room. I thought of JavaScript as a strange little language that barely even qualified as “real programming.”
Well, a lot has changed in the 20 years since then. I now see in JavaScript what Douglas Crockford saw when he wrote JavaScript: The Good Parts: “An outstanding, dynamic programming language … with enormous, expressive power.”
So, without further ado, here is a wonderful little pattern I’ve been using in my code lately. I hope you come to enjoy it as much as I have.
Please note : I’m pretty sure I did not invent any of this. Chances are I came across it in other people’s code and eventually adopted it myself.
Receive an object, return an object (RORO)
Most of my functions now accept a single parameter of type object
and many of them return or resolve to a value of type object
as well.
Thanks in part to the destructuring feature introduced in ES2015, I’ve found this to be a powerful pattern. I’ve even given it the silly name, “RORO” because… branding? 🤷♂️
Note: Destructuring is one of my favorite features of modern JavaScript. We’re going to be taking advantage of it quite a bit throughout this article, so if you’re not familiar with it, here’s a quick video from Beau Carnes to get you up to speed.
Here are some reasons why you’ll love this pattern:
- Named parameters
- Cleaner default parameters
- Richer return values
- Easy function composition
Let’s look at each one.
Named Parameters
Suppose we have a function that returns a list of Users in a given Role and suppose we need to provide an option for including each User’s Contact Info and another option for including Inactive Users, traditionally we might write:
function findUsersByRole (
role,
withContactInfo,
includeInactive
) {...}
A call to this function might then look like:
findUsersByRole(
'admin',
true,
true
)
Notice how ambiguous those last two parameters are. What does “true, true” refer to?
What happens if our app almost never needs Contact Info but almost always needs Inactive Users? We have to contend with that middle parameter all the time, even though it’s not really relevant (more on that later).
In short, this traditional approach leaves us with potentially ambiguous, noisy code that’s harder to understand and trickier to write.
Let’s see what happens when we receive a single object instead:
function findUsersByRole ({
role,
withContactInfo,
includeInactive
}) {...}
Notice our function looks almost identical except that we’ve put braces around our parameters. This indicates that instead of receiving three distinct parameters, our function now expects a single object with properties named role
, withContactInfo
, and includeInactive
.
This works because of a JavaScript feature introduced in ES2015 called Destructuring.
Now we can call our function like this:
findUsersByRole({
role: 'admin',
withContactInfo: true,
includeInactive: true
})
This is far less ambiguous and a lot easier to read and understand. Plus, omitting or re-ordering our parameters is no longer an issue since they are now the named properties of an object.
For example, this works:
findUsersByRole({
withContactInfo: true,
role: 'admin',
includeInactive: true
})
And so does this:
findUsersByRole({
role: 'admin',
includeInactive: true
})
This also makes it possible to add new parameters without breaking old code.
One important note here is that if we want all the parameters to be optional, in other words, if the following is a valid call…
findUsersByRole()
… we need to set a default value for our parameter object, like so:
function findUsersByRole ({
role,
withContactInfo,
includeInactive
} = {}) {...}
An added benefit of using destructuring for our parameter object is that it promotes immutability. When we destructure the object
on its way into our function we assign the object’s properties to new variables. Changing the value of those variables will not alter the original object.
Consider the following:
const options = {
role: 'Admin',
includeInactive: true
}
findUsersByRole(options)
function findUsersByRole ({
role,
withContactInfo,
includeInactive
} = {}) {
role = role.toLowerCase()
console.log(role) // 'admin'
...
}
console.log(options.role) // 'Admin'
Even though we change the value of role
the value of options.role
remains unchanged.
It’s worth noting that destructuring makes a_ shallow _copy so if any of the properties of our parameter object are of a complex type (e.g. array
or object
) changing them would indeed affect the original.
Cleaner Default Parameters
With ES2015 JavaScript functions gained the ability to define default parameters. In fact, we recently used a default parameter when we added ={}
to the parameter object on our findUsersByRole
function above.
With traditional default parameters, our findUsersByRole
function might look like this.
function findUsersByRole (
role,
withContactInfo = true,
includeInactive
) {...}
If we want to set includeInactive
to true
we have to explicitly pass undefined
as the value for withContactInfo
to preserve the default, like this:
findUsersByRole(
'Admin',
undefined,
true
)
How hideous is that?
Compare it to using a parameter object like so:
function findUsersByRole ({
role,
withContactInfo = true,
includeInactive
} = {}) {...}
Now we can write…
findUsersByRole({
role: ‘Admin’,
includeInactive: true
})
… and our default value for withContactInfo
is preserved.
BONUS: Required Parameters
How often have you written something like this?
function findUsersByRole ({
role,
withContactInfo,
includeInactive
} = {}) {
if (role == null) {
throw Error(...)
}
...
}
Note: We use double equals (==) above to test for both null
and undefined
with a single statement.
What if I told you that you could use default parameters to validate required parameters instead?
First, we need to define a requiredParam()
function that throws an Error.
Like this:
function requiredParam (param) {
const requiredParamError = new Error(
`Required parameter, "${param}" is missing.`
)
// preserve original stack trace
if (typeof Error.captureStackTrace === ‘function’) {
Error.captureStackTrace(
requiredParamError,
requiredParam
)
}
throw requiredParamError
}
I know, I know. requiredParam doesn’t RORO. That’s why I said many of my functions — not all .
Now, we can set an invocation of requiredParam
as the default value for role
, like so:
function findUsersByRole ({
role = requiredParam('role'),
withContactInfo,
includeInactive
} = {}) {...}
With the above code, if anyone calls findUsersByRole
without supplying a role
they will get an Error
that says Required parameter, “role” is missing.
Technically, we can use this technique with regular default parameters as well; we don’t necessarily need an object. But this trick was too useful not to mention.
Richer Return Values
JavaScript functions can only return a single value. If that value is an object
it can contain a lot more information.
Consider a function that saves a User
to a database. When that function returns an object it can provide a lot of information to the caller.
For example, a common pattern is to “upsert” or “merge” data in a save function. Which means, we insert rows into a database table (if they do not already exist) or update them (if they do exist).
In such cases, it would be handy to know wether the operation performed by our Save function was an INSERT
or an UPDATE
. It would also be good to get an accurate representation of exactly what was stored in the database, and it would be good to know the status of the operation; did it succeed, is it pending as part of a larger transaction, did it timeout?
When returning an object, it’s easy to communicate all of this info at once.
Something like:
async saveUser({
upsert = true,
transaction,
...userInfo
}) {
// save to the DB
return {
operation, // e.g 'INSERT'
status, // e.g. 'Success'
saved: userInfo
}
}
Technically, the above returns a Promise
that resolves to an object
but you get the idea.
Easy Function Composition
“Function composition is the process of combining two or more functions to produce a new function. Composing functions together is like snapping together a series of pipes for our data to flow through.” — Eric Elliott
We can compose functions together using a pipe
function that looks something like this:
function pipe(...fns) {
return param => fns.reduce(
(result, fn) => fn(result),
param
)
}
The above function takes a list of functions and returns a function that can apply the list from left to right, starting with a given parameter and then passing the result of each function in the list to the next function in the list.
Don’t worry if you’re confused, there’s an example below that should clear things up.
One limitation of this approach is that each function in the list must only receive a single parameter. Luckily, when we RORO that’s not a problem!
Here’s an example where we have a saveUser
function that pipes a userInfo
object through 3 separate functions that validate, normalize, and persist the user information in sequence.
function saveUser(userInfo) {
return pipe(
validate,
normalize,
persist
)(userInfo)
}
We can use a rest parameter in our validate
, normalize
, and persist
functions to destructure only the values that each function needs and still pass everything back to the caller.
Here’s a bit of code to give you the gist of it:
function validate(
id,
firstName,
lastName,
email = requiredParam(),
username = requiredParam(),
pass = requiredParam(),
address,
...rest
) {
// do some validation
return {
id,
firstName,
lastName,
email,
username,
pass,
address,
...rest
}
}
function normalize(
email,
username,
...rest
) {
// do some normalizing
return {
email,
username,
...rest
}
}
async function persist({
upsert = true,
...info
}) {
// save userInfo to the DB
return {
operation,
status,
saved: info
}
}
To RO or not to RO, that is the question
I said at the outset, most of my functions receive an object and many of them return an object too.
Like any pattern, RORO should be seen as just another tool in our tool box. We use it in places where it adds value by making a list of parameters more clear and flexible and by making a return value more expressive.
If you’re writing a function that will only ever need to receive a single parameter, then receiving an object
is overkill. Likewise, if you’re writing a function that can communicate a clear and intuitive response to the caller by returning a simple value, there is no need to return an object
.
An example where I almost never RORO is assertion functions. Suppose we have a function isPositiveInteger
that checks wether or not a given parameter is a positive integer, such a function likely wouldn’t benefit from RORO at all.
If you enjoyed this article, please tap the ♥️ icon to help spread the word. And if you want to read more stuff like this, please sign up for my Dev Mastery newsletter below.
Sign up to the DevMastery Newsletter
I keep your info private and I NEVER spam.
Top comments (13)
I think the biggest problem I have with using this approach as a matter of course is that, great, you have a simple signature and can return whatever you want, but unless you put some serious effort into generating and maintaining documentation, it's impossible to use your function effectively without understanding it in its entirety. A signature that enumerates its arguments tells you how to use it, making API docs an improvement rather than a necessity. Primitive return values are similarly easier to process.
It makes sense if you're really dealing in, say, configuration objects, states, and other naturally complex values. But that's because functions that operate on complex objects are understood in complex terms. When you come into
render()
, decomposition works great because you're thinking about yourprops
and yourstate
. Replacing ordinary arguments and return values is a different story: it may make a function look nicer at first glance, but it's harder to work with in any context larger than the function by itself -- which includes practically every case except the unit tests.Thank you for your feedback Dian.
I first encountered the concept of receiving a single object rather than individual parameters back when jQuery ruled the world. Some functions in jQuery – especially those related to
$ajax
– received a so-called options object.Back then, I didn't like this pattern at all for the exact reasons you mentioned; I found that it made functions more opaque and added complexity that could only be overcome by adding more documentation.
Since then, two things happened which changed my mind. The first is the advent of destructuring syntax. The second, equally important change, is in the sophistication of our code editors.
For example, here is how VS Code treats my
findUserByRole
function. No extra documentation needed.I use options objects myself in Massive -- it's the only way to keep signature size under a sane maximum when you have a dozen different things that can affect the shape of your results and don't want to move to a builder-style pattern. The tradeoff is that I have to be very careful with documentation; that and the fact that the options are a known set applied consistently across the API are why it's usable at all.
Destructuring is more a convenience than a compelling argument for working like this. You could always pass objects and return objects; now you can express it a bit more elegantly, but it's still the same concept. What really enables you is your editor, and that's not something I'd be willing to depend on. People use other editors with different featuresets, and even if everyone standardized on VSCode, syntax tree analysis isn't available when you're reading diffs or browsing an open source codebase on the web. Readability as plain text is critical, and sacrificing that to achieve an architecture that actually increases local complexity seems like a bad trade for most cases.
Thanks again for your feedback.
I would argue that we are actually increasing plain text readability rather than diminishing it.
In terms of reading the function signature, I don't think the extra curly braces impede readability. And, in terms of reading code that consumes our function, I think a destructured parameter object has the advantage.
Imagine coming across the following examples in plain text...
...isn't the second one clearer? Especially when we are in a plain text environment that can't easily navigate to the function's signature.
Essentially what you have with this example is a criteria object. This is another instance where the idea works out okay, because you're approaching the unary argument as criteria, not as a discrete role and flags. You don't even really need destructuring to work with it: you can just iterate
Object.keys
and pop the values into your prepared statement parameters or analogous structure.Where the real problems start to pop up is when you bundle information that shouldn't be bundled. Think
dispatchMessage('channel', 'message', true)
versusdispatchMessage({channel: 'channel', message: 'message', emitOnReceipt: true})
. It's true that you can make an educated guess as to whatemitOnReceipt
does without having to look atdispatchMessage
's signature. But are there other behaviors? Canmessage
itself be a complex object? What happens if I pass a "meta-message" object that contains x, y, z other fields (even if the function destructures arguments immediately,arguments
is still around to make things interesting)?A well-formed signature does as well with some of these kinds of questions as what's arguably an abuse of anonymous objects, and does better with others; notably, the possibility of pollution is obviated. If you're going to operate on a "meta-message" concept that ties these disparate values together, it should be a proper class. And sometimes it's worth doing that! But throwing anonymous objects around is something that really needs to be considered carefully.
These kinds of API usage issues can be avoided by using flow or TypeScript and a suitable editor. Code completion will tell you exactly what you can put into the options argument, and what to expect from the result. The actual downside in this approach lies elsewhere: constructing these additional "message" objects takes time. So I would not advice using this technique too excessively, especially in performance-critical regions of the code.
Great to have you here Bill!
Great to be here :)
I love this pattern and use this almost all the time. It's obviously informative and clearer to read.
The only time it causes problem is when we have to write same arguments over and over in different lines.
Clear and convincing. Currently updating my code ... :-)
I have used this approach for a while which I did not even think it as a pattern.Thank your point.
I really like this article, it is pretty good explained and the examples are very clear and easy to understand :)
Not sure I'd use it in many places but definitely interesting. But how did I not know about that required parameter pattern?? Definitely adding that everywhere.