I Prefer The Unary Plus (+) Operator Over parseInt() And ParseFloat() When Coercing Strings To Numbers In JavaScript
Yesterday, I accidentally stumbled into the location of a bug that's existed in InVision for over 5-years. And, when I finally found the problematic line of code, I felt like it warranted a write-up on its own. Because, the "broken" code wasn't inherently broken; rather, it was just using an approach that was more prone to subtle failures. The line of code in question was using parseInt()
to try and coerce a String
value into a Number
value. For this type of operation, I almost always prefer the unary plus (+
) operator over parseInt()
or parseFloat()
in JavaScript.
To be clear, I am not saying that the unary plus operator is equivalent to the parseInt()
or the parseFloat()
methods. The unary plus operator and these methods work in different ways and have different features. What I am saying is that in the most common use-cases, I believe that the unary plus operator more clearly represents the intent of the engineer writing the code; and, is less prone to subtle failures when faced with unexpected inputs.
To paint a picture of the bug I found, here's the line of React code that I found:
parseInt( $target.attr( 'data-group-id' ), 10 )
As you can see, it was just using a jQuery reference to grab an attribute out of the DOM (Document Object Model); and then, parse that attribute value into a number. The problem with this code (in my application) is that there's an edge-case in which the given attribute contains a temporary UUID. Something like:
cb34d-234ks-2343f-00xj
Now, if you try to run this value through parseInt()
, you get NaN
(Not a Number):
parseInt( "cb34d-234ks-2343f-00xj", 10 )
=> NaN
But, the UUID is generated with random digits. Which means, passing the UUID to the parseInt()
function will "fail" in different ways depending on the characters in the UUID. For example, if we try parsing another randomly-generated UUID, we end up with a number:
parseInt( "997da-00xj-2343f-234ks", 10 )
=> 997
Both the parseInt()
and the parseFloat()
methods parse the input string until they hit a non-numeric value, at which point they discard the rest of the input. So, to be clear, this is parseInt()
working exactly how it was designed to work.
The problem is, this is almost certainly not what the original engineer intended. The intent of the engineer was simply to convert a String into a Number; and, if the String didn't represent a Number, the resultant value should be NaN
.
In the vast majority of cases in which I need to convert a String to a Number, I've learned to prefer the unary plus operator because it's much short from a syntax perspective and it more closely maps to the intention of type coercion:
// When used with a temporary UUID, the characters in the string don't matter -
// the unary plus operator will always return NaN.
console.log( +"cb34d-234ks-2343f-00xj" ); // => NaN
console.log( +"997da-00xj-2343f-234ks" ); // => NaN
// The unary plus operator works with both integer and float inputs.
console.log( +"16" ); // => 16
console.log( +"3.14159" ); // => 3.14159
Now, it's worth noting that the unary plus operator in JavaScript also has some subtle behaviors. As I learned from this Scotch.io post, the unary plus operator can parse special string formatting:
// It will parse scientific notation that contains "e".
console.log( +"1.2345e10" ); // => 12345000000
// It will parse hex values that start with "0x".
console.log( +"0xff" ); // => 255
// It will parse Infinity.
console.log( +"Infinity" ); // => Infinity
console.log( +"-Infinity" ); // => -Infinity
In my mind, however, this is still different than the behavior of parseInt()
and parseFloat()
because these string inputs are at least intended to represent a number. And, unlike parseInt()
and parseFloat()
, the unary plus operator will still return NaN
when the special input contains any unexpected characters:
console.log( +"1.2345e10___" ); // => NaN
console.log( +"0xff___" ); // => NaN
console.log( +"Infinity___" ); // => NaN
console.log( +"-Infinity___" ); // => NaN
So, going back to the bug that I found in the React code, all I had to do in order to fix the issue was replace the parseInt()
call with the unary plus operator:
+$target.attr( 'data-group-id' )
In my JavaScript programming, parseInt()
and parseFloat()
still have a time and a place. For example, I still use parseInt()
to parse base-16 input (hex) into decimal value, such as with color conversion. However, in the vast, vast majority of cases in which I have to coerce a String
value into a Number
value in JavaScript, I prefer the unary plus operator. It's short; it fails more consistently; and, I believe it maps more closely to my intent.
Want to use code from this post? Check out the license.
Reader Comments
@All,
After writing this, I've been struggling a bit with why I don't put a space after the
+
sign. For example, if I were to use one of the other unary operators, like the!
or!!
(which really isn't an operator in and of itself), I would always put a space between the operator and the operand). Example:I would never in a million years place the
!
operator up against its operand like!values.length
... so, why am I placing the+
operator up against its operand?In fact, adding the space may help draw attention to it, which may make it more obvious:
Hmmm. Of course, it starts to get weird if you're ever going to use the plus binary operator along side the unary operator:
Of course, you could just argue that combining this into a single-statement is bad form, regardless of whether or not it is "correct".
Hmmm, something to think about. I'll have to play around with it a bit more in real-world code to get a sense of whether or not the operator feels ok being pulled-out a space.
@All,
Worth mentioning, a number of people on Twitter responded that they like to use the
Number()
constructor for such things. As in:I don't think there's anything wrong with that. To be honest, I just don't have a lot of experience using the native constructors in my day-to-day JavaScript. Probably, I have the most experience with
RegExp()
; however, I still prefer to use the literal,/pattern/flags
, when possible.So, anyway, I still prefer the unary operator; but, I thought it was worth sharing the
Number()
approach as well.Hi Ben, Nice find. I saw your tweet on twitter but was not able to grasp it. This article explains it nicely. Personally I prefer using + instead of Number constructor.
The operator seems slightly faster that the function: according to this test.
@JM,
While I would generally suggest taking benchmarks with a grain-of-salt, I think it make sense that the
+
operator is faster thanNumber()
. I believe - and hopefully I am not misremembering here - that there is actually a difference between a primitive value like3
and an Object like the one produced byNumber( 3 )
. From what I believe I have read, JavaScript uses the primitive values in the code; and then, as needed, will automatically wrap the primitive value in the relevant constructor when it needs to do things like access member methods.So, what I think that means it that when you do something like this in JavaScript:
... the JavaScript interpreter is actually implicitly wrapping it to be something like:
But don't quote me on this :P All to say, I assume / suspect that the
+
operator is generating a primitive value, not aNumber
instance.@Hassam,
Groovy -- glad this made sense :)
I prefer using
Number('100')
, because it's an explicit function call, so it's easier to read. It's functionally identical to unary+
... but using the unary operator is much harder to read and feels like a hack.There is a microscopic performance advantage to
+
, if that outweighs code quality, but that's the only difference I can find.A separate thing I wanted to mention, is that
Number/String/Boolean
should only ever be treated as casting functions. I have never seen a valid use case for usingnew Number/String/Boolean
, which is what gives you a boxed value. This post explains it pretty well ...new Number(x)
is bad,Number(x)
is good,+x
is a shortcut.https://stackoverflow.com/a/4719334/272072
@Scott,
Very interesting. I had always just assumed that the
new
was quasi-optional for the built-in constructors. Meaning, I thoughtRegExp()
andnew RegExp()
were essentially doing the exact same thing. It's good to know that they are in fact different.I'll have to mull-over how I feel about calling the casters. I'm not opposed to it - I'm just not used to it. For example, I use the
!!
double-not operator to case to Boolean all the time. As in:I've been doing that for so long that the idea of converting to the more explicit cast feels funky:
I'll let that marinate in the back of my mind. Thanks for the clarity :D
@JM,
The benchmark is misleading because the +"100" case is benefiting from some compiler optimizations that wont happen when the input is dynamic.
See this updated version
@Jcon,
Ah, excellent catch! Damn that compiler, always trying to help :P That said, that's exactly why I don't put tooooo much heed into micro-bench-marks. In the end, you often just need to go with developer ergonomics. But, it's great to see how roughly equivalent they are.
@Ben,
"I would never in a million years place the ! operator up against its operand like !values.length"
Why in the world not? That's just... utterly bizarre.
@Rabo,
Just personal preference. With the
!
up against the operand, it just looks wrong to me. I generally prefer more white-space where is makes sense. But, I know not everyone is white-space-sensitive.What about preferring Number() over + even though there is a small performance hit because of readability?
I generally think of code as: "What would I understand from this, would I see this in 5 years", and: "What would a novice javascript/typescript developer understand from this right now?".
Asking those questions, Number() - for me - definitely wins: it reads better, even though it is more verbose.
@Michaël,
I think that's fair. I think it's just personal preference on what looks right.