Type coercion is the automatic or implicit conversion of values from one data type to another (MDN). Developers who come from statically-typed languages usually dislike type coercion. They think it makes their code too unpredictable. TypeScript aims to "fix" JavaScript for these developers. It has its place: TypeScript powers the IntelliSense for JavaScript in Visual Studio Code. But generally, it "fixes" problems I don't have. To be fair, I don't write much JavaScript "at scale". But used thoughtfully, I think type coercion is a feature, not a bug!

Numbers as strings

Let's start by looking at numbers. Specifically, using numbers where strings are expected. Even if you don't like type coercion, I bet you use it all the time when you access an element in an array by its index:

const letters = ["a", "b", "c"];

// Type coercion is happening here!
const a = letters[0];

A few weeks ago, we learned why the Array.prototype.at() method exists. I've taken the following quote straight from the proposal for the .at() method:

The [] syntax is not specific to Arrays and Strings; it applies to all objects. Referring to a value by index, like arr[1], actually just refers to the property of the object with the key "1", which is something that any object can have. So arr[-1] already "works" in today's code, but it returns the value of the "-1" property of the object, rather than returning an index counting back from the end.

Object properties can't be numbers; they can only be strings or symbols. When you access an element in an array by its index, you're relying on type coercion! Unless you're explicitly using a string value:

const letters = ["a", "b", "c"];

// No type coercion here...
const a = letters["0"];

Let's look at another common example: setting an element's .textContent property. Having to explicitly convert between numbers and strings is annoying! It's easier to rely on type coercion:

const REGION_ID = "count";

const liveRegion = document.getElementById(REGION_ID);
const button = document.querySelector(`[aria-controls='${REGION_ID}']`);

// The value of the `.textContent` property is a string.
let count = liveRegion.textContent;

function increment() {
// The ++ operator can only be used with numbers, so type coercion happens here.
// It happens in reverse when we assign the value of `count` to the `.textContent` property.
liveRegion.textContent = ++count;
}

button.addEventListener("click", increment);

Demo - Numbers as strings.

Booleans as strings

Treating booleans as strings is useful too. For example, when implementing an accessible toggle button, you don't have to explicitly convert the boolean value. The Boolean.prototype.toString() method is implicitly called:

const ARIA_PRESSED = "aria-pressed";

const toggleButton = document.querySelector(`[${ARIA_PRESSED}]`);

function toggle() {
const isPressed = toggleButton.getAttribute(ARIA_PRESSED) === "true";

// We don't have to explicitly convert the boolean value of `isPressed` to a string.
toggleButton.setAttribute(ARIA_PRESSED, !isPressed);
}

toggleButton.addEventListener("click", toggle);

Demo - Booleans as strings.

Type coercion goes with the grain

I think it's fair to say that type coercion goes with the grain of JavaScript, while enforcing types goes against the grain. For example, if we wanted to write a sum() function, we could enforce that all arguments be numbers:

function sum(...nums) {
return nums.reduce((sum, num) => {
const type = typeof num;

if (type !== "number") {
throw new TypeError(`Expected number; received ${type}.`);
}

return sum + num;
}, 0);
}

sum("1", [2]); // TypeError: Expected number; received string.

I think it's more natural to use type conversion. If an argument can be converted to a number, then it will be. While this uses explicit type conversion instead of implicit type coercion, it feels more resilient than throwing an error:

function sum(...nums) {
return nums.reduce((sum, num) => sum + Number(num), 0);
}

sum("1", [2]); // 3
sum("1", {}); // NaN

David Heinemeier Hansson (DHH) can be divisive, but my comments here remind me of something he says in The Rails Doctrine:

Ruby accepts both exit and quit to accommodate the programmer's obvious desire to quit its interactive console. Python, on the other hand, pedantically instructs the programmer how to properly do what's requested, even though it obviously knows what is meant (since it's displaying the error message).

In our sum() function, we know that the programmer is trying to add numbers together, so why not help them out? Being pedantic and throwing an error just makes the code more brittle—and verbose!

Arguments against type coercion

I think type safety in a large codebase is a sensible argument against type coercion. But arguments against type coercion typically use nonsensical examples, such as these from the famous 'wat' talk:

([] + []); // ""
([] + {}); // "[object Object]"
({} + []); // "[object Object]"
({} + {}); // "[object Object][object Object]"

I appreciate the humour of the talk. But this is meaningless noise, because no one actually does this. Regardless, let's examine what's happening here.

Array + Array

When we try to add two array literals together, the Array.prototype.toString() method is implicitly called on both arrays. Because both arrays are empty, it's like concatenating two empty strings. Thus, we get an empty string as the final result: "".

Array + Object

When we try to add an array literal and an object literal together, the Array.prototype.toString() and Object.prototype.toString() methods are implicitly called.

Because the array is empty, the former returns an empty string (""). The latter returns a string representing the object ("[object Object]").

Concatenating these two strings gives us the final string "[object Object]".

Object + Array

This example may behave differently depending on the implementation of the JavaScript engine.

Within parentheses, this example is treated as an expression, so the result is the same as the previous example: "[object Object]".

Without parentheses, the curly braces ({}) may be treated as an empty block statement. This evaluates to the primitive value undefined, which is a valid expression, and therefore affected by automatic semicolon insertion. This causes the plus sign (+) to be treated as the unary plus operator, which precedes its operand and evaluates to its operand but attempts to convert it into a number, if it isn't already (MDN). Thus, undefined; +[]; evalues to 0.

We can verify this using the global eval() method. We have to manually insert the semicolon after the call to eval(), because eval("{}") +[]; is already a valid expression, and thus the call to eval() is unaffected by automatic semicolon insertion:

eval("{}"); +[]; // 0

The reason why we don't get the string "undefined" is because the primitive value undefined doesn't have a corresponding wrapper object. There is no Undefined object; therefore, there is no Undefined.prototype.toString() method to be implicitly called. It would only be possible if we explicitly converted the undefined value to a string:

String(undefined) + []; // "undefined"

Object + Object

This is another example that may behave differently depending on the engine.

Within parentheses, this example is treated as an expression. The Object.prototype.toString() method is implicitly called for each object literal, so we end up concatenating two instances of the string "[object Object]". This leaves us with the string "[object Object][object Object]".

Without parentheses, both sets of curly braces ({}) may be treated as empty block statements. Both of these evaluate to undefined, so we end up trying to add two instances of the primitive value undefined together. This results in NaN (Not a Number).

We can verify this using the global eval() method:

eval("{}") + eval("{}"); // NaN

Like the previous example, the reason why we don't get the string "undefinedundefined" is because there is no Undefined.prototype.toString() method to be implicitly called. It would only be possible if we explicitly converted at least one of the primitive values to a string:

String(undefined) + undefined; // "undefinedundefined"

undefined + String(undefined); // "undefinedundefined"

String(undefined) + String(undefined); // "undefinedundefined"

Summary

With the exception of the esoteric explanations (yay for alliteration), the examples in this blog post have been straightforward. If I haven't convinced you to appreciate type coercion, that's absolutely fine. I can totally see the benefit that type safety brings to a large codebase. If nothing else, I hope this was a fun thought experiment!