The Curse of Coercive Typing

February 4, 2021

In Javascript, all values can be type coerced to be evaluated in a boolean expression. So what this means in practice is that every data type has some notion of being true or false. Integers and floats are "false" if their value is 0. Strings are "false" if they have no characters, as are the values null, undefined, and NaN. Any initialized structure or object is "true", even the empty ones like [] and {}. Over time, people came to use the words "truthiness" and "falsiness" to describe this type of behavior.

Side note: This harkens back to the days of C, when there were no booleans built into the language. In C, every value has an implicit "truthiness", determined by whether or not it's equal to bitwise 0. Comparisons return 1 for true and 0 for false.

This concept allows us to do cool and convenient things to make our code more succinct. For example, instead of while (x > 0) {...}, one could just say while (x) {...}. Or instead of if (someObject != null) {...}, just if (someObject) {...}. You see this type of test everywhere, and this is one of the many constructs that Javascript has that allows for amazingly flexible and fast code writing, and lets you to feel like a ninja while writing (for better or for worse).

"Okay", you say, "okay cool, that makes sense". Yes it sure does. But, just go to your browser (I tried this on Opera, Chrome, Edge, and Firefox), open up the developer console, and type document.all. You might notice the expression return type comes back as the aptly titled HTMLAllCollection which contains all the elements on the current document. Log it and you'll see the collection returned. Assign it to a variable and you'll see the expression return the collection again. "Cool, a document collection object", you think.

Okay, now do:

typeof document.all;

What did you get? Surprised?

The Cold Hard Truth (or not so much)

The MDN docs contains a table with a complete list of "falsy" values. Looking through the table, you see all the usual suspects: false, 0, -0, empty strings, BigInt's 0n, and so on. And then at the bottom, there's document.all sitting there by itself. The right side of the table offers up:

"Objects are falsy if and only if they have the [[IsHTMLDDA]] internal slot. That slot only exists in document.all and cannot be set using JavaScript."

Following the link on [[IsHTMLDDA]] takes you to the offical ECMAscript specification which opaquely declares:

"An [[IsHTMLDDA]] internal slot may exist on host-defined objects."

Well that doesn't help at all...

In essence, ES2015 uses the construct of internal slots to set the behavior of different objects by attaching values and methods to named fields. The spec says that an [[IsHTMLDDA]] internal slot may exist on host-defined objects, which are any objects provided by the host environment (like window, document, and family in a browser, or module in Node). But then it notes that "...implementations should not create any with the exception of document.all." So why is this field the only outlier here? By now, you may have noticed that part of the field name "HTMLDDA" looks suspiciously close to an abbreviation for "HTML Document.All". Is it that this is a shim to make document.all act in a different way for some reason?

And indeed, the reason behind all this craziness is a need for compatibility. The document.all field was provided in really old browsers as a way to modify and access parts of the document, but it was superceded by more modern ways of doing things like document.getElementById(). Older webpages would often test first for document.all to see if they could use it as their interface, using the most succinct test possible, courtesy of Javascript's coercive typing. In more modern browsers, this meant that better ways of doing things were being passed up in favor of document.all because it was simply asked for first.

if (document.all) {
// use document.all
} else if (betterInterfaceExists) {
// we never make it here...
}

Now this could simply be solved if developers tested for the other interface first, but there were already so many webpages testing for document.all first that wouldn't be fixed that way unless the developers went and switched it themselves, but getting that to happen would have been pretty much impossible.

So the solution that many browsers implemented (and that was worked into ECMAScript with the [[IsHTMLDDA]] slot) was to make document.all falsy by modifying its abstract equality and toBoolean behaviors. That way, if any script tested to see if document.all existed before using it, the script would be told that it doesn't, and it would pass through.

if (document.all) {
// since document.all is falsy,
// this code doesn't execute
} else if (document.getElementById) {
// so we use this
}

And for browsers which weren't polite enough to check, the functionality was still there for them to use. You can still see that in your browser; although it evaluates to undefined and coerces to false, if you just type document.all the expression will evaluate to an HTMLAllCollection. You could detect it with if ("all" in document) since they didn't fix that because no one really did it that way. It's important to note that this field has been deprecated ever since the release of HTML5 and is not dependable or guaranteed to exist. In most modern browsers, it's just internally shimmed to a different interface so it can be used like normal, but it's definitely not the best or safest option.

Is it a big deal? Unless you're trying to write Javascript with support for early IE and Netscape Navigator in mind, it won't affect your day-to-day in the slightest. It's just an artifact of an older age tucked up away from the modern code world for legacy scripts to grab and use if necessary. But it is an interesting how widespread use of a common feature test built up such a problem that the behavior, and eventually the fundamentals, of the language had to be changed just to solve the problem. It's a fix that wormed its way to the heart of Javascript and when all was said and done, truth was left a little less sure.

"The truth is rarely pure and never simple" - Oscar Wilde