Skip to content

Instantly share code, notes, and snippets.

@jonschlinkert
Last active February 8, 2024 08:26
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jonschlinkert/e30c70c713da325d0e81 to your computer and use it in GitHub Desktop.
Save jonschlinkert/e30c70c713da325d0e81 to your computer and use it in GitHub Desktop.

Unary operators

(and the oddities of number evaluation in JavaScript)

Type conversion, typecasting, and coercion are different ways of, implicitly or explicitly, changing an entity of one data type into another. --wikipedia


Unary operators, or "typeof +'foo' === huh?"

A unary operator requires a single operand, either before or after the operator --MDN: JavaScript Guide > Expressions and operators

Beware the "leading plus"!. Althought unary operators can be very useful for converting strings to numbers, they can also yield results that you don't expect if you're not aware of the side effects of how they work.

example

typeof ({})  //=> 'object'
typeof (+{}) //=> 'number'
typeof ('\n') //=> 'string'
typeof (+'\n') //=> 'number'

!!({})       //=> 'true'
!!(+{})      //=> 'false'

Unary operators offer the fastest, simplest way to coerce string-numbers to real integers. The point I want to make has less to do with unary operators themselves, and more to do with how the use of unary operators might lead to unintended consequences, depending on how they're used.

To keep this focused, all of the following examples will use the unary plus operator, but note that - works the same way, in reverse.

Here is what the MDN docs have to say about the hero(ine) of our story:

unary plus is the fastest and preferred way of converting something into a number, because it does not perform any other operations on the number. It can convert string representations of integers and floats, as well as the non-string values true, false, and null.

Wow! that g(al|uy) sounds pretty amazing! Let's see why...


***

A number is a number. Right?

"Hold it, can I see some typeof ID? Oh your a "10", yeah you can go in (..these strings drive me nuts, always trying to act like real numbers. Makes my job a pain in the...) heyyy, heyyy, hey, stop right there buddy. ID. What's your name? "null"? yeah you look like "null" (hehe, ..looks like nul..) What? "plus null"? Oh excuuuse me, I didn't see the "plus". Anyway, sorry, can't let you... what? Where?... (huh I guess his ID says he's a number, and it looks legit but he sure don't look a real number. something seems fishy, but all these arguments look the same to me. what do I ..oh maybe I should ask for another typeof..). Yeah I heard you buddy. Look, your ID checks out, says you're a number so I'm gonna let you go through. But no trouble wise guy... I got my eye on you. Next..."

We often want quoted strings, like "100", to count as numbers. But since strings that happen to look like numbers aren't actually numbers, and using simple checks like typeof "100" === 'number' return false, it's often necessary to coerce strings to numbers.

The unary + or - operators exist exactly for this purpose. For example, the following returns false

typeof '100' === 'number' //=> false

Adding a unary plus before "100" makes the statement true:

typeof +'100' === 'number' //=> true

This is great! Makes complete sense. After all, unary operators are supposed to do this, right? Well, not so fast. We should get something out of the way early, there is very little mystery around what the unary plus operator does: put a single + before any value, and whatever it is, typeof +____ will return number.

In other words, a leading + (or -) makes everything evaluate to number:

typeof +'foo'          //=> 'number'
typeof +true           //=> 'number'
typeof +false          //=> 'number'
typeof +null           //=> 'number'
typeof +undefined      //=> 'number'
typeof +{}             //=> 'number'
typeof +[]             //=> 'number'
typeof +function({})   //=> 'number'
typeof +new Date()     //=> 'number'
typeof +new Buffer('') //=> 'number'
typeof +new Array('')  //=> 'number'
typeof +Infinity       //=> 'number'

Even this:

typeof +NaN //=> 'number'

Use whatever values you want, knowing that you can rely on the fact taht typeof +__whatever_values_you_want__ will always return number.

It gets a little harder to follow when you see the actual values produced by our type coercion rampage:

+true           //=> '1'
+false          //=> '0'
+null           //=> '0'
+undefined      //=> 'NaN'
+{}             //=> 'NaN'
+[]             //=> '0'
+function({})   //=> 'NaN'
+'foo'          //=> 'NaN'
+new Date()     //=> '1422091853924'
+new Buffer('') //=> '0'
+new Array('')  //=> '0'
+NaN            //=> 'NaN'
+Infinity       //=> 'Infinity'

Perhaps this trick doesn't seem so reliable now.

It's still a great trick to use if you know the arguments being passed will consistently be either integers or numbers-as-strings. But you will need to implement additional checks to ensure that null, undefined or other unwanted values don't slip through.

You can easily create your own, but if you're like me and prefer using modules, here is my own "is-number" implementation.

Hopefully by now I've already convinced you of the brittleness of numerical type checking in JavaScript. If I haven't, please read on. The below examples do the trick.

More oddities

isFinite

isFinite() is the native JavaScript method to check whether or not a number is finite. It's probably one of the better standalone ways to check for numbers. But, it still shouldn't be used alone.

Remember, we can't get a reliable result with when non-numbers can be coerced to numbers.

isFinite('')                //=> 'true'
isFinite({})                //=> 'false'
isFinite(+{})               //=> 'false'
isFinite('0')               //=> 'true'
isFinite('1000')            //=> 'true'
isFinite(null)              //=> 'true' (really?)
isFinite(+null)             //=> 'true' (hmmm)
isFinite('null')            //=> 'false'
isFinite(+'null')           //=> 'false'
isFinite(undefined)         //=> 'false'
isFinite('undefined')       //=> 'false'
isFinite(+'undefined')      //=> 'false'
isFinite({})                //=> 'false'
isFinite(+{})               //=> 'false'
isFinite([])                //=> 'true' 
isFinite(+[])               //=> 'true' 
isFinite(+new Array())      //=> 'true'
isFinite(+new Array(0))     //=> 'true' 
isFinite(new Array(0))      //=> 'true'
isFinite(new Array(0)+1)    //=> 'true'
isFinite(+new Array(1))     //=> 'true'
isFinite(new Array(1)+1)    //=> 'true'
isFinite(+new Array(1)+1)   //=> 'true'
isFinite(+new Array(2))     //=> 'false'
isFinite(+new Array(2)+1)   //=> 'false'
isFinite(+new Array(100)+1) //=> 'false'
isFinite(typeof (+new Array(1)) === "number") //=> 'true'

Instead of using isFinite(), we can achieve similar results to isFinite() by "going all smart-like" with some actual math, like ((+n+1) / (+n+1) === 1)). But even this isn't bulletproof since JavaScript allows values like null to pass as numbers.

null and undefined

null

null can be coerced to a number:

null     //=> null (obviously)
+null    //=> 0 (not so obvious)
+null+1  //=> 1
null+1   //=> 1

undefined

Not so much with undefined

+undefined                       //=> NaN
+undefined+1                     //=> NaN
typeof (+undefined) === 'number' //=> true

(not a number, uuuuunless you do typeof, that is...)

Arrays

Arrays literals are coerced to numbers:

+[]                 //=> 0
+[1]                //=> 1
+[2]                //=> 2
+[10]               //=> 10

But not with an instance of the Array constructor:

+new Array()      //=> '0'
+new Array(0)     //=> '0'
new Array(0)      //=> '[]'
new Array(0)+1    //=> '1'
+new Array(1)     //=> '0'
new Array(1)+1    //=> '1'
+new Array(1)+1   //=> '1'
+new Array(2)     //=> 'NaN'
+new Array(2)+1   //=> 'NaN'
+new Array(100)+1 //=> 'NaN'
typeof (+new Array(100)+1) === "number" //=> 'true'

(unless you're passing strings...)

Buffers

How about an instance of Buffer?

+new Buffer(+'foo')                          //=> '0'
+new Buffer(+'foo') + 1                      //=> '1'
+new Buffer('foo')                           //=> 'NaN'
+new Buffer('foo') + 1                       //=> 'NaN'
new Buffer('foo') + 1                        //=> '1'
typeof (new Buffer('foo')) === 'number'      //=> 'false'
typeof (new Buffer(+'foo')) === 'number'     //=> 'false'
typeof (+new Buffer('foo')) === 'number'     //=> 'true'
typeof (+new Buffer('foo') + 1) === 'number' //=> 'true'

To recap, apparently +new Buffer('foo') + 1 is NaN, until it's evaluated by typeof, in which case it becomes a number?

I'm... Ron Burgundy?

Strings

What about strings?

+'foo'    //=> NaN
+'foo'+1  //=> NaN
typeof +'foo' === 'number' //=> true huh?

RegExp

/\d/.exec(9)      //=> '[ '9', index: 0, input: '9' ]'
/\d/.test(9)      //=> true
/\d/.test('9')    //=> true
9.match(/\d/)     //=> 'SyntaxError: Unexpected token ILLEGAL'
'9'.match(/\d/))  //=> '[ '9', index: 0, input: '9' ]'

Operator overloading

With integers, we get what one would expect:

+0    //=> 0
+1    //=> 1
+100  //=> 100

When a string is added to an integer, the integer is treated like a string and the values are concatenated:

'10' + '10'       //=> '1010'
'10' + 10         //=> '1010'
10 + '10'         //=> '1010'
'10' + (+'10')    //=> '1010'

By now you know that if the strings represent numbers, we can use operator overloading to coerce them to actual integers (note that the parentheses (e.g. (+'10')) are unnecessary but I like using them for clarity).

10 + (+'10')      //=> '20'
(+'10') + (+'10') //=> '20'

That's it for now!

Please let me know if you have an example you'd like to add! Since GitHub doesn't notify when someone posts to a gist, please feel free to ping me on Twitter: @jonschlinkert.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment