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
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:
Now, if you try to run this value through
parseInt(), you get
NaN (Not a Number):
parseInt( "cb34d-234ks-2343f-00xj", 10 ) =>
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 ) =>
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
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
// 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
parseFloat() because these string inputs are at least intended to represent a number. And, unlike
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' )
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
Want to use code from this post? Check out the license.
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
!!(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.
Worth mentioning, a number of people on Twitter responded that they like to use the
Number()constructor for such things. As in:
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.
While I would generally suggest taking benchmarks with a grain-of-salt, I think it make sense that the
+operator is faster than
Number(). I believe - and hopefully I am not misremembering here - that there is actually a difference between a primitive value like
3and an Object like the one produced by
But don't quote me on this :P All to say, I assume / suspect that the
+operator is generating a primitive value, not a
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/Booleanshould only ever be treated as casting functions. I have never seen a valid use case for using
new Number/String/Boolean, which is what gives you a boxed value. This post explains it pretty well ...
new Number(x)is bad,
+xis a shortcut.
Very interesting. I had always just assumed that the
newwas quasi-optional for the built-in constructors. Meaning, I thought
new 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
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
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.
"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.
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?
Asking those questions, Number() - for me - definitely wins: it reads better, even though it is more verbose.
I think that's fair. I think it's just personal preference on what looks right.