Default parameters and nulls in ES6
Parameter defaults are only impactful for undefined
in ES6 JS. The MDN docs are clear as day here…
Default function parameters allow formal parameters to be initialized with default values if no value or
undefined
is passed.
…yet I have still been burned at least a few times on not appreciating that null
is going to be handled differently than undefined
. Consider the code below:
const computeTotal = ({ parts = [] } = {}) => (
parts.reduce((total, part) => total + part, 0)
);
This function seems to be alright:
computeTotal({ parts: [1, 2] }); // 3
computeTotal({}); // 0
computeTotal(); // 0
Yet it completely fails given a null
parts
.
computeTotal({ parts: null });> TypeError: Cannot read property 'reduce' of null
parts
could be null
in a range of situations. One easy example — a row in a database with a null
column value.
computeTotal
needs to handle null
, and defaulting parameters is useless since it only handles undefined
. I think defaulting the parameter AND handling null
in the body of the function — while it solves the problem — is possibly confusing to future code readers:
const computeTotal = ({ parts = [] } = {}) => (
(parts || []).reduce((total, part) => total + part, 0)
);
It would be easy to think parts || []
was some leftover cruft from a refactor or redundant, even though it has a different effect — parts || []
protects against all falsey values and not just undefined
. Similarly
const computeTotal = ({ parts } = {}) => (
(parts || []).reduce((total, part) => total + part, 0)
);
might cause a future developer to refactor the function to the failing function for the same reason. So what is the right way to do this?
My first recommendation would be to always have unit tests that test the null scenario. This will at least stop others from making refactors that break the code.
My second recommendation would be to use a more verbose check for null
or undefined
values, and stop protecting against false
, empty strings and NaNs
. If parts
is any of these values, something is likely wrong with the data and I want to know about it (note that this whole example operates on the assumption that computeTotal
takes an object returned by a database driver). So, I’m in favor of:
const computeTotal = ({ parts } = {}) => (
(parts != null ? parts : []).reduce(
(total, part) => total + part, 0
)
);
At least in this example, because a null
or undefined
parts
will always return 0
for the total, this really should be changed to:
const computeTotal = ({ parts } = {}) => (
parts != null ? parts.reduce((total, part) => total + part, 0) : 0
);
Finally, if handling all falsey values is important, I think at least a comment, some type checking or really any signaling within the code about possible values for parts
makes sense.
Like this post? Check out my blog, subscribe to my reading recommendation newsletter or let’s get in touch about consulting, pair programming, or anything tech-related on your mind.