Type Coercion In JavaScript: A Deep Dive

Rajat Saxena

July 8, 2023

Type conversion (also known as coercion) in JavaScript confuses a lot of developers and makes a significant chunk of interview questions and general trivia/gotchas. In this post, we will learn the basic building blocks to coercion and see how to apply those to actual real world problems.

Type coercion is JavaScript's attempt to be fault-tolerant and forgiving. It will try to convert the data types of the variables you are performing an operation upon so that the operation does not error out.

Let's establish the first building block.

1. Everything gets converted to either a number or a string ultimately.

JavaScript will apply a few rule and ultimately all the operands will be converted to either of these values.

1.1. How does JavaScript decide if it should convert an operand to a string or a number?

Glad that you asked! There are certain operations which prefer numbers and others which prefer strings. Let's see a few examples.

1.1.1. Operations that prefer numbers

*       // multiply

%       // modulo

++      // prefix (or postfix) operators

Listing 1

1.1.2. Operations thats prefer strings

`${}`         // string templates

String()      // creating an string object

Listing 2

Then there are operations which are neutral like, addition and loose equality (==). We will learn more about those in Section 3.

So how does JavaScript convert any value to number/string? For that, we are going to establish the second building block.

2. JavaScript uses a function to convert anything to either number or string

JavaScript uses a function called ToPrimitive which converts any input to a primitive value. If the input is already a primitive, it is returned as is. Otherwise, it is first converted into a primitive value and then returned.

JavaScript has the following primitives.

  • string

  • number

  • bigint

  • boolean

  • undefined

  • symbol

  • null

JavaScript calls ToPrimitive function automatically when the type conversion happens. While calling this function, JavaScript also sends the preference i.e. whether it would like to convert that value into a string or a number, as an input. This input is called hint and its value can be string | number|default.

Following is the over-simplified pseudo-code of ToPrimitive. Please keep in mind I have glossed over non-essential stuff and we will follow the same pattern in all of such examples.

ToPrimitive (input, hint: string|number|default = default):
    if input is an object:
        if input has a function called toPrimitive:
            result = toPrimitive(input, hint)
            if result is not an object:
                return result
            else:
                throw error

        if hint == default:
            hint = number

        return OrdinaryToPrimitive(input, hint)

    // already a primitive
    return input

Listing 3

Here is what the above pseudo-code is doing.

  1. If the hint is not defined then it is initialised to default.

  2. If the input is an object, it will follow Step #3 and beyond. Otherwise, it will return the input as it is already a primitive.

  3. If the input has a method called toPrimitive, it will call that method with the hint.

  4. If the result of Step #3 is an not an object, it will return this result. Otherwise, it will throw an error (TypeError in particular)

  5. If the input does not has a method called toPrimitive, first the hint is set to number and then a function call is made to OrdinaryToPrimitive.

In most cases, this toPrimitive function is not defined hence the responsibility lands on OrdinaryToPrimitive function to convert an object into a primitive.

Following is the implementation in over-simplified pseudo-code.

OrdinaryToPrimitive(hint: string|number):
    if hint == "string"
        methods = ["toString", "valueOf"]
    else
        methods = ["valueOf", "toString"]

    loop over methods array:
        if method is callable:
            result = method()
            if result is not of object type:
                return result
            else
                throw error

Listing 4

So as you can see from above, JavaScript will call two methods i.e. toString and valueOf in a preferred sequence depending upon the hint.

2.1. Primitives will further converge to either string or number

We saw that ToPrimitive will return primitive values. These primitive values will be ultimately converted to string/number. JavaScript uses ToNumber and ToString for this. These methods define how values like null, undefined, symbol, bigint etc. are converted to numbers and strings respectively.

2.2. Hands-on

Let's see the above concepts in action. For that let's define a variable as follows.

let b = {
    valueOf: function () { return 1 },
    toString: function () { return 'bee' }
}

Listing 5

Now, if we do the following

b * 2 // prints 2

Listing 6

This is because the multiplication operator prefers the operands to be numbers. Hence JavaScript tried to convert the object b to a number. For this, it invokes ToPrimitive with number hint, which in turn calls OrdinaryToPrimitive with number hint and hence valueOf is invoked.

Let's try something else.

`${b}` // prints "bee"

Listing 7

Again, using the above logic and section 1.1.2, toString gets invoked and hence the result.

3. Confusing operators

There are operators which are neutral i.e. to don't prefer string over numbers and vice-versa. The following are a few such operators.

  1. Addition (+)

  2. Loose equality (==)

  3. Less than (<)

This class of operators leads to the most confusion around coercion in JavaScript. We will see how addition and loose equality works in this post. Less than is somewhat more complex than the other two so we will leave that out for now.

3.1. Addition

Again, let's look at the pseudo-code of how this is implemented.

Addition(x, y):
    convert operands to primitives
    if any operand is string:
        convert both operands to string
        concatenate the operands
        return result
    else:
        convert all operands to numbers
        if any operand is unconvertible (symbol, bigint):
            throw error
        else
            add both operands
            return result

Listing 8

This one is simple.

  1. If any operand is a string, convert both to string, concatenate both and return the result.

  2. Otherwise, convert both to numbers.

  3. If there is an error in converting to numbers, throw error. Otherwise, add both and return the result.

3.1.1. Hands-on

Let's the try the easier ones first.

'e' + 1 // prints 'e1'
1 + 2   // prints 3

Listing 9

Now, let's try a complex one.

let b = {
    valueOf: function () { return 1 },
    toString: function () { return 'bee' }
}

b + 1 // prints 2

Listing 10

Here, one operand i.e. b is an object and the other one is a number. JS will try to convert both of these to primitive (see Listing 8, line 2). To convert b to primitive, ToPrimitive will be invoked with the default hint. We know the if the hint is default, it is overridden with number (see Listing 3, line 10). Hence b will be coerced to a number. So, valueOf is invoked which returned 1.

Now, let's try adding b to a string

b + '1' // prints "11"

Listing 11

Since '1' is a string, hence b will be coerced to a string (see Listing 8, lines 3-4). Hence, toString will be invoked from b.

One more

true + false // prints 1

Listing 12

Here, both of the operands will be converted to numbers using ToNumber (see Listing 8, line 8) and hence the result.

3.2. Loose Equality

Brace yourself, this operation's pseudo-code is considerably trickier. The values x' and y' denote the values after the conversion.

LooseEquality(x, y):
    If both operands are of same types, do strict comparison (===)

    if comparing null and undefined:
        return true

    if comparing number and string:
        convert the string operand to number
        LooseEquality(x', y')

    if comparing bigint and string:
        convert the string operand to bigint
        if the above results in NaN:
            return false
        LooseEquality(x', y')

    if any operand is boolean:
        convert boolean operands to number
        LooseEquality(x', y')

    if one operand is object and the other is one of [string, number, bigint, symbol]:
        convert the object to primitive value
        LooseEquality(x', y')

    if comparing number and bigint:
        if any operand is non-finite i.e. NaN, +Infinity, -Infinity:
            return false
        if the mathematical values of both operands are the same:
            return true
        else:
            return false

    return false

Listing 13

Here are some easily deduced take-aways from the above algorithm.

  1. If the types are same, Strict equality operator (===) is used.

  2. Comparing null to undefined will result in true.

  3. While comparing a string to a number, the string operand is converted to a number.

  4. Boolean values are converted to numbers.

  5. Objects are converted to primitive values.

3.2.1. Hands-on

3.2.1.1. Let's try this.

1 == '1' // prints true

Listing 14

The string operand '1' gets converted to 1 (see Listing 13, line 8) and the equality operator is invoked again with arguments (1, 1). Due to Listing 13, line 2, we get true.

3.2.1.2.

let d = {
    valueOf: function () { return 0 },
    toString: function () { return 'bee' }
}

d == 0 // prints true

Here, d will be converted to a primitive value (see Listing 13, line 21-22). ToPrimitive will be called with the default hint which means that the it will be coerced to a number (see Listing 3, line 10). Hence, we get 0 == 0 which is true.

3.2.1.3.

[] == 0     // prints true
[] == false // prints true

Arrays are objects so [] gets converted to 0 (Listing 13, line 21-22). Hence the results.

That's it! If you have questions, come discuss this over Twitter or in my Discord server.

Become that "Pro" full stack developer at your job

Receive our best tips, tutorials, videos and interview questions right in your inbox, weekly. Grow as a full stack dev. A little, every week. 🚀