Common JS noob mistakes with type coercion, and how to avoid them
A short preface:
Lately in Vets Who Code, our head honcho, Jerome Hardaway, has been posting little code challenges, for the purpose of warming everyone up each morning, and getting our new troops prepared for the cohort we have starting in September.
These are not meant to be difficult problems, just a little something to wake up everyone’s brains in the morning.
However, my brain does not like to be woken up in the morning. I work from home, and tend to work late, into the wee hours of morning. More commonly than not, I tend to rise about an hour before my first morning meeting.
By the time standup rolls around, my brain’s still attempting to assimilate the caffeine from my first two cups of Death Wish Coffee (not a paid promo, I just love their high-octane brew, and they deserve the publicity).
Wake my brain up earlier then it’s prepared to, and my cranky ass becomes possessed with a desire to rip shit up. This goes against best practices for working at home, I know. I cop to the fact that I, like many others, struggle to maintain the discipline and motivation it takes to maintain such a lifestyle. I’m working on it, but there are still some mornings where I’m not quite there yet.
It was when I was in this state of mind that I started to notice some common mistakes which appeared in quite a few of the submissions. Keep in mind, the folks that make these submissions have not started our program yet, they’re waiting their turn, so I really shouldn’t be so hard on them. However, it’s in their best interests to learn this stuff early, so with that in mind, let’s get started…
An example problem:
Write a JS function to calculate the sum of two given numbers.
If either param or the sum of both params are equal to 50, return _ _true
Sounds simple right?
At first glance, something like this may seem passable:
function fiftyOrSumOfFifty (num1, num2) {
if (num1 == 50 || num2 == 50) {
return true;
}
else if (num1 + num2 == 50) {
return true;
}
else {
return false;
}
}
Note: This was not taken from a specific student’s code, I’m not trying to call anybody out on the quarterdeck. Rather, this was the most common solution I saw, cleaned up a little, formatting-wise… and it’s not surprising, really. Given the fact that the students had little to no prior instruction outside of the pre-work they’d done to be accepted into the program, I’m actually quite happy to see something like it, even with its issues.
It even works, if you feed it the right params…
console.log(fiftyOrSumOfFifty(25, 25)) // returns `true`
console.log(fiftyOrSumOfFifty(25, 0)) // returns `false`
console.log(fiftyOrSumOfFifty(50, 0)) // returns `true`
console.log(fiftyOrSumOfFifty(4, 50)) // returns `true`
However, like I told you before, the sweetness of my creamer and that awesome caffeine buzz (thanks again, Death Wish!) had yet to overcome the bitterness of my morning funk, and I decided to start feeding it params it might not like. This leads to common JS noob mistake numero uno:
- Not accounting for parameter type
JS is a weakly-typed language. As opposed to strongly-typed languages, which enforce the type of params allowed1, JS lets you stick any old thing into the params, and will happily feed you an answer, if it’s capable. However, that answer may not be the one we’re looking for. For example, in the above code, what happens if we feed a string to the function fiftyOrSumOfFifty?
console.log(fiftyOrSumOfFifty('50', 0)) // returns `true`
// and...
console.log(fiftyOrSumOfFifty('5', '0')) // also returns `true`
console.log(fiftyOrSumOfFifty('5', 0)) // also returns `true`
So, here’s what’s happening: as I said before, JS does not care if you feed it the wrong type of params. Because it’s a loosely-typed language, it will accept anything for a param.
ANYTHING.
You could feed it an Integer, Float, String, Boolean, Array, or nothing at all (null) if you wanted, and if it's able to do something with that param that fits into the code you've written, JS will return something, without throwing an error. In the first case above where we submit a string, we're submitting a value that can be coerced to a type that can indeed equal 50, and so returns true. That actually leads into the next common mistake, and we'll get to that in a second. It's related to this one, but in a much more specific way. In the third string example, we're submitting '5', a string, and 0, an integer. Why does this return true? Because you can use the + operator on a string, and it will work.
It just won't sum the two params3.
When you use the + operator on a pair of strings, it concatenates the strings; that is, it sticks the second directly on to the tail end of the first. In the example function, this means that eventually, because neither param is equivalent to 50, the params get passed on to the summing operation, which, because we have provided a string for one of the params, now becomes a concatenation operation, and '5' + 0 evaluates as '50'.
Why does this work the same in the second example as in the third, where we are submitting two params of different types? Because JS utilizes a trick called
type coercion... if handed two vars of different types in an operation like +, it will attempt to convert one of the vars into the first type that makes sense, which in this case means that both are treated as strings, and the 0 gets tacked on to the '5' as '0', resulting in '50', which will return true.
But wait a minute… if the + operator converted it to a string, why is that
'50' (a string, that is, text), evaluating as equal to 50 (a number)?
That should not be!
Well, yes and no… I’m coming to that, stop being so impatient.
First, remember how I told you that you could also slip an Array into the params? Well, one way you can get around the whole string concatenation bit I mentioned previously, is to parse your params as floats (ex: parseFloat('15') // returns 15)... when you do this to an Array, however, something interesting happens:
console.log(parseFloat(['50', 25])) // returns... 50??? WTH!!!
When you use parseFloat on an Array, it only parses the first item in the Array! Does the weirdness never cease? This surprised me, and I only just discovered this oddness as I was typing out this article, as I'd honestly never thought to try parsing an Array as a Float before. It’s actually so unheard-of to do this, it’s not even given a mention in MDN’s documentation. Just goes to show you can learn something new every day. I decided to avoid the issue altogether, and if either param was an array (ex: Array.isArray(num1)...), I'd just return false.
Anyways, getting back to type coercion…
- === > ==
The example function is evaluating equality using the == ('double-equals') operator, which infers type. This means that, like the + operator coerces the type of the var and changes its function from 'sum' to 'concatenate', the double-equals operator is looking at the two vars and saying to itself: “Hey, one of these things is not like the other... but maybe it could be, if they had the same type? Let's try that!”
Since '50'
can be parsed as 50
, '50' == 50
evaluates true. This is actually a pretty easy mistake to make, especially if coming over to JS from another language, as most other programming languages don't do this with their double-equals operator - the ==
operator in most other languages will function much like the ===
(triple-equals) operator does in JS, and will perform strict equivalence evaluation... that is, a string will NEVER be equal to a number, no matter how similar the text inside the string may be. In other languages, the ===
operator does not necessarily function the same way; For instance, in Ruby, ===
can be used to compare type9
String === 'bacon' # evaluates to `true`
…and so, a JS newbie, when coming from Ruby, may not expect it to function
the way it does in JS, or even be aware of its existence. In Ruby, ==
does what ===
does in JS. Excusable, if you're new to the language, but use of ==
to check equivalence is a habit that should be broken swiftly when training new JS devs, lest they fall into a bad habit. There are just too many side-effects that using this operator can produce. Instead of using ==
, ===
(triple-equals) should be used when checking equality, as it does not perform any type-conversion2.
Speaking of bad habits that ought to be broken swiftly:
- Excessive conditionals and returns, or, ‘Failure to refactor’
This can be attributed to lack of experience. Refactoring ones code to be more efficient eventually comes to be a habit, but it’s one that must be learned and self-enforced. The given example uses one if to check a condition and return true if that condition returns true, then uses else if to check another statement and return true if that statement returns true, then finally utilizes an else statement to return false if all else fails. Think about that sentence for a moment. The conditions themselves are already returning a boolean value. We don't need the conditional, at all. With the application of the ||
(logical 'or'6](#6)) operator, we can do:
return (condition1) || (condition2) || (condition3)
...and avoid the use of the conditional statements, altogether.
Wrapping up:
Here’s my first solution to the problem; it allows string params, so long as they can parse to floats and pass the conditions, to return true. I chose to do that because JS is primarily meant for the web, and if you’re doing DOM manipulation, it’s all text until you parse it to be otherwise (or use a number input, I guess; if you want the whole truth, I over-thought it). It will not, however, allow Arrays, undefined, or null, or any other data type that I’ve thought of aside from numbers and strings, to eval to true:
const numOrSumFifty = (num1, num2) =\> !anyArr(num1, num2) && any50(num1, num2)
const any50 = (num1, num2) =\> {
return numIs50(num1) || numIs50(num2) || numIs50(sum(num1, num2))
}
const anyArr = (num1, num2) =\> Array.isArray(num1) || Array.isArray(num2)
const numIs50 = num =\> parseFloat(num) === 50
const sum = (num1, num2) =\> parseFloat(num1) + parseFloat(num2)
However, had I chosen not to allow string params (which I probably should have done in the first place; over-engineering is an anti-pattern in and of itself, after all), this function could easily boil down to something much, much simpler:
const numOrSumFifty =
(num1, num2) =\> num1 === 50 || num2 === 50 || num1 + num2 === 50
Remember what I said about failure to refactor? Always check yourself, kids.
I wrote my functions out in repl.it 8 as Jest 7 examples, rather than straight JS examples, so I could write test suites for the functions (You can find the first here, and the ‘no-string-params’ example here). I invite you to write your own tests for the suites in a fork and try to break my functions. If you can think of a case I haven’t, I’ll be more than happy to update my code, and credit you for the find; I love peer review! None of us is above reproach, and if I’ve gotten something wrong, I’d rather know and be able to fix it, than be left in the dark.
And with that, I hope I’ve shed a little light on some of the mistakes one can commonly make with Javascript’s loose typing as a novice JS dev (and a couple even a pro can make, if they’re not careful). Until next time!
References:
- Strong and weak typing | ↩
- Loose equality and sameness | ↩
- Arithmetic Operators | ↩
- parseFloat | ↩
- isArray | ↩
- Logical Operators | ↩
- Jest: Delightful Javascript Testing | ↩
- Repl.it | ↩
- Ruby operators equality comparison | ↩
Top comments (0)