Inside JavaScript: Understanding Type Coercion
Inside JavaScript: Understanding Type Coercion
Previous | Next | |
---|---|---|
Type Coercion: The Sensible Bits | Up | Type Coercion: The Unusual Bits |
Type Coercion: The Mostly Sensible Bits
A Value’s “Truthiness” or “Falsiness”
Let’s take this snippet of JavaScript code and see what it gives us:
// Create an object whose properties hold a value of each data type
var datatypes = {
"true" : true,
"false" : false,
"zero" : 0,
"one" : 1,
"null" : null,
"undefined" : undefined,
"NaN" : NaN,
"emptyString" : "",
"function" : function() {},
"emptyObject" : {},
"emptyArray" : [],
}
// Loop around each property in the object coercing its value to Boolean,
// then writing it to the console
for (var i in datatypes) {
console.log(`datatypes["${i}"] = ${!!datatypes[i]}`);
}
The double NOT !!
is a trick that coerces a value to its Boolean equivalent.
datatypes["true"] = true // As expected
datatypes["false"] = false // Again, as expected
datatypes["zero"] = false // Yup, I know about this one
datatypes["one"] = true // And this one
datatypes["null"] = false // Yeah, that makes sense
datatypes["undefined"] = false // Makes sense too
datatypes["NaN"] = false // Ok, I'll go with that
datatypes["emptyString"] = false // Thinking about it, that makes sense too
datatypes["function"] = true // Uh, ok (but why?)
datatypes["emptyObject"] = true // Seriously!?
datatypes["emptyArray"] = true // Now that's just silly!
Some Terminology
Any value that coerces to true
is said to be truthy
Any value that coerces to false
is said to be falsey
So, not surprisingly, values like 0
, null
and undefined
all coerce to false
and are therefore said to be falsey. In fact, with a little explanation, most of these type coercions give the result you might expect. The general principle here is that any value that can be thought of as being “empty” will coerce to false
, and any value that can be thought of as “containing something” will coerce to true
.
Slightly Sensible…
Q: Ok, so why would function
coerce to true
?
A: Because a function is a special type of executable object, so even if the function does nothing (as in our example above), it is never truly empty.
Perhaps less intuitively, even though they might be empty in the sense of “having zero properties”, all JavaScript Objects are truthy.
But This Is JavaScript…
The real screwball here is the Array
.
As we have already seen, JavaScript treats an Array
simply as an Object
.
So even though that Array might contain zero elements, as far as Boolean coercion is concerned, it is treated as an object, and all objects are truthy — but more of this later…
The Slightly Silly typeof
operator
Most of the time, the JavaScript typeof
operator gives you back a reasonably useful description of a variable’s datatype.
For example:
// Declare some variables to have various data types
var aNumber = 123
var aString = "Nothing to see here, move along"
var anObject = { aProperty : 0 }
var aFunction = function() { }
// What does JavaScript think these data types are?
typeof aNumber // 'number'
typeof aString // 'string'
typeof anObject // 'object'
typeof aFunction // 'function'
Ok, fair enough, no surprises here.
But what about these?
// Declare some more variables
var anArray = [1,2,3,4,5]
var notANumber = NaN
var nullValue = null
// What does JavaScript think the data types are?
typeof anArray // 'object' Well, I guess...
typeof notANumber // 'number' Say what?
typeof nullValue // 'object' But that's just wrong!
Having typeof
return number
for something that is explicitly not a number isn’t quite as weird as you might think.
However, you’ll have to read the ECMAScript specification to discover why—specifically sections 4.3.24 and 7.1.3.
While it’s annoying for typeof
to tell you that an Array
is just an object
, this answer is, in fact, accurate—albeit unhelpful.
However, telling me that null
is an object is totally misleading!
A Sensible Version of the typeof
Operator
Many widely used libraries provide their own functionality to replace JavaScript’s built-in typeof
operator.
For instance, jQuery provides the type
function that gives back an accurate answer no matter what you throw at it:
var anArray = [1,2,3,4,5]
var nullValue = null
// jQuery provides a robust fix for JavaScript's only-sometimes-helpful typeof operator
jQuery.type(anArray) // 'array'
jQuery.type(nullValue) // 'null'
// Or the nice predicate function
jQuery.isArray(anArray) // true
However, if you’re not using any such library, it’s easy enough to create your own version of typeof
that will always give you an accurate answer:
const typeOf = x => Object.prototype.toString.apply(x).slice(8).slice(0, -1)
Explanation
The above code works as follows:
-
We declare a constant called
typeOf
that, using the arrow syntax, is of typefunction
.> const typeOf = x => ...
This function takes a single argument
x
that represents the thing whose datatype we wish to discover. -
> const typeOf = x => Object.prototype.toString...
As with all JavaScript objects, the universal object
Object
inherits its properties from aprototype
that contains (among other things) a function calledtoString
that returns a printable representation of the object to which it belongs.However, if we directly called
Object.prototype.toString()
, it would return the string representation ofObject.prototype
—which is not what we want. So we call theapply
function belonging to functiontoString
and supply the argumentx
received by ourtypeOf
function.The
apply
function provides an indirect means for callingx.toString()
without needing to know exactly whatx
is. -
> const typeOf = x => Object.prototype.toString.apply(x) > typeOf("") '[object String]' > typeOf(null) '[object Null]'
We now have the full string representation of whatever value we pass to
typeOf
. -
However, as you can see, this string contains extra characters that are of no interest to us. So the last thing to do is chop off the first 8 characters using
slice(8)
, then chop off the last character usingslice(0,-1)
.const typeOf = x => Object.prototype.toString.apply(x).slice(8).slice(0, -1)
Now we have a custom typeOf
function that returns a character string containing the actual datatype of whatever value it is passed.
In addition, running this function without passing any argument returns the accurate response Undefined
.
Datatype Predicate Functions
Our custom typeOf
function can now be used as the foundation to create simple predicate functions:
// Return the actual datatype of the argument
const typeOf = x => Object.prototype.toString.apply(x).slice(8).slice(0, -1)
// Partial function that creates a function to check for a specific data type
const isOfType = t => x => typeOf(x) === t
// Primitive type identifiers
const isNull = isOfType("Null")
const isUndefined = isOfType("Undefined")
const isNumber = isOfType("Number")
const isBigInt = isOfType("BigInt")
const isSymbol = isOfType("Symbol")
const isArray = isOfType("Array")
const isMap = isOfType("Map")
const isSet = isOfType("Set")
const isString = isOfType("String")
const isFn = isOfType("Function")
const isGenFn = isOfType("GeneratorFunction")
const isJsObject = isOfType("Object")
If you’re running JavaScript in NodeJS, there are two special system objects that return their name rather than their type when you use this typeOf
function: process
and global
.
Hence it is worthwhile creating two more predicate functions that handle these specific cases:
// The NodeJS objects 'global' and 'process' return their own names when asked their type
// even though they are just regular objects
const isNodeJsProcess = isOfType("process")
const isNodeJsGlobal = isOfType("global")