Deep JavaScript
Please support this book: buy it or donate
(Ad, please don’t block.)

2 Type coercion in JavaScript



In this chapter, we examine the role of type coercion in JavaScript. We will go relatively deeply into this subject and, e.g., look into how the ECMAScript specification handles coercion.

2.1 What is type coercion?

Each operation (function, operator, etc.) expects its parameters to have certain types. If a value doesn’t have the right type for a parameter, three common options for, e.g., a function are:

  1. The function can throw an exception:

    function multiply(x, y) {
      if (typeof x !== 'number' || typeof y !== 'number') {
        throw new TypeError();
      }
      // ···
    }
  2. The function can return an error value:

    function multiply(x, y) {
      if (typeof x !== 'number' || typeof y !== 'number') {
        return NaN;
      }
      // ···
    }
  3. The function can convert its arguments to useful values:

    function multiply(x, y) {
      if (typeof x !== 'number') {
        x = Number(x);
      }
      if (typeof y !== 'number') {
        y = Number(y);
      }
      // ···
    }

In (3), the operation performs an implicit type conversion. That is called type coercion.

JavaScript initially didn’t have exceptions, which is why it uses coercion and error values for most of its operations:

// Coercion
assert.equal(3 * true, 3);

// Error values
assert.equal(1 / 0, Infinity);
assert.equal(Number('xyz'), NaN);

However, there are also cases (especially when it comes to newer features) where it throws exceptions if an argument doesn’t have the right type:

2.1.1 Dealing with type coercion

Two common ways of dealing with coercion are:

I usually prefer the former, because it clarifies my intention: I expect x and y not to be numbers, but want to multiply two numbers.

2.2 Operations that help implement coercion in the ECMAScript specification

The following sections describe the most important internal functions used by the ECMAScript specification to convert actual parameters to expected types.

For example, in TypeScript, we would write:

function isNaN(number: number) {
  // ···
}

In the specification, this looks as follows (translated to JavaScript, so that it is easier to understand):

function isNaN(number) {
  let num = ToNumber(number);
  // ···
}

2.2.1 Converting to primitive types and objects

Whenever primitive types or objects are expected, the following conversion functions are used:

These internal functions have analogs in JavaScript that are very similar:

> Boolean(0)
false
> Boolean(1)
true

> Number('123')
123

After the introduction of bigints, which exists alongside numbers, the specification often uses ToNumeric() where it previously used ToNumber(). Read on for more information.

2.2.2 Converting to numeric types

At the moment, JavaScript has two built-in numeric types: number and bigint.

Table 1: Coercion of the operands of bitwise number operators (BigInt operators don’t limit the number of bits).
Operator Left operand Right operand result type
<< ToInt32() ToUint32() Int32
signed >> ToInt32() ToUint32() Int32
unsigned >>> ToInt32() ToUint32() Uint32
&, ^, | ToInt32() ToUint32() Int32
~ ToInt32() Int32

2.2.3 Converting to property keys

ToPropertyKey() returns a string or a symbol and is used by:

2.2.4 Converting to Array indices

2.2.5 Converting to Typed Array elements

When we set the value of a Typed Array element, one of the following conversion functions is used:

2.3 Intermission: expressing specification algorithms in JavaScript

In the remainder of this chapter, we’ll encounter several specification algorithms, but “implemented” as JavaScript. The following list shows how some frequently used patterns are translated from specification to JavaScript:

let (and not const) is used to match the language of the specification.

Some things are omitted – for example, the ReturnIfAbrupt shorthands ? and !.

/**
 * An improved version of typeof
 */
function TypeOf(value) {
  const result = typeof value;
  switch (result) {
    case 'function':
      return 'object';
    case 'object':
      if (value === null) {
        return 'null';
      } else {
        return 'object';
      }
    default:
      return result;
  }
}

function IsCallable(x) {
  return typeof x === 'function';
}

2.4 Example coercion algorithms

2.4.1 ToPrimitive()

The operation ToPrimitive() is an intermediate step for many coercion algorithms (some of which we’ll see later in this chapter). It converts an arbitrary values to primitive values.

ToPrimitive() is used often in the spec because most operators can only work with primitive values. For example, we can use the addition operator (+) to add numbers and to concatenate strings, but we can’t use it to concatenate Arrays.

This is what the JavaScript version of ToPrimitive() looks like:

/**
 * @param hint Which type is preferred for the result:
 *             string, number, or don’t care?
 */
function ToPrimitive(input: any,
  hint: 'string'|'number'|'default' = 'default') {
    if (TypeOf(input) === 'object') {
      let exoticToPrim = input[Symbol.toPrimitive]; // (A)
      if (exoticToPrim !== undefined) {
        let result = exoticToPrim.call(input, hint);
        if (TypeOf(result) !== 'object') {
          return result;
        }
        throw new TypeError();
      }
      if (hint === 'default') {
        hint = 'number';
      }
      return OrdinaryToPrimitive(input, hint);
    } else {
      // input is already primitive
      return input;
    }
  }

ToPrimitive() lets objects override the conversion to primitive via Symbol.toPrimitive (line A). If an object doesn’t do that, it is passed on to OrdinaryToPrimitive():

function OrdinaryToPrimitive(O: object, hint: 'string' | 'number') {
  let methodNames;
  if (hint === 'string') {
    methodNames = ['toString', 'valueOf'];
  } else {
    methodNames = ['valueOf', 'toString'];
  }
  for (let name of methodNames) {
    let method = O[name];
    if (IsCallable(method)) {
      let result = method.call(O);
      if (TypeOf(result) !== 'object') {
        return result;
      }
    }
  }
  throw new TypeError();
}
2.4.1.1 Which hints do callers of ToPrimitive() use?

The parameter hint can have one of three values:

These are a few examples of how various operations use ToPrimitive():

As we have seen, the default behavior is for 'default' being handled as if it were 'number'. Only instances of Symbol and Date override this behavior (shown later).

2.4.1.2 Which methods are called to convert objects to Primitives?

If the conversion to primitive isn’t overridden via Symbol.toPrimitive, OrdinaryToPrimitive() calls either or both of the following two methods:

The following code demonstrates how that works:

const obj = {
  toString() { return 'a' },
  valueOf() { return 1 },
};

// String() prefers strings:
assert.equal(String(obj), 'a');

// Number() prefers numbers:
assert.equal(Number(obj), 1);

A method with the property key Symbol.toPrimitive overrides the normal conversion to primitive. That is only done twice in the standard library:

2.4.1.3 Date.prototype[Symbol.toPrimitive]()

This is how Dates handle being converted to primitive values:

Date.prototype[Symbol.toPrimitive] = function (
  hint: 'default' | 'string' | 'number') {
    let O = this;
    if (TypeOf(O) !== 'object') {
      throw new TypeError();
    }
    let tryFirst;
    if (hint === 'string' || hint === 'default') {
      tryFirst = 'string';
    } else if (hint === 'number') {
      tryFirst = 'number';
    } else {
      throw new TypeError();
    }
    return OrdinaryToPrimitive(O, tryFirst);
  };

The only difference with the default algorithm is that 'default' becomes 'string' (and not 'number'). This can be observed if we use operations that set hint to 'default':

This is the JavaScript version of ToString():

function ToString(argument) {
  if (argument === undefined) {
    return 'undefined';
  } else if (argument === null) {
    return 'null';
  } else if (argument === true) {
    return 'true';
  } else if (argument === false) {
    return 'false';
  } else if (TypeOf(argument) === 'number') {
    return Number.toString(argument);
  } else if (TypeOf(argument) === 'string') {
    return argument;
  } else if (TypeOf(argument) === 'symbol') {
    throw new TypeError();
  } else if (TypeOf(argument) === 'bigint') {
    return BigInt.toString(argument);
  } else {
    // argument is an object
    let primValue = ToPrimitive(argument, 'string'); // (A)
    return ToString(primValue);
  }
}

Note how this function uses ToPrimitive() as an intermediate step for objects, before converting the primitive result to a string (line A).

ToString() deviates in an interesting way from how String() works: If argument is a symbol, the former throws a TypeError while the latter doesn’t. Why is that? The default for symbols is that converting them to strings throws exceptions:

> const sym = Symbol('sym');

> ''+sym
TypeError: Cannot convert a Symbol value to a string
> `${sym}`
TypeError: Cannot convert a Symbol value to a string

That default is overridden in String() and Symbol.prototype.toString() (both are described in the next subsections):

> String(sym)
'Symbol(sym)'
> sym.toString()
'Symbol(sym)'
2.4.2.1 String()
function String(value) {
  let s;
  if (value === undefined) {
    s = '';
  } else {
    if (new.target === undefined && TypeOf(value) === 'symbol') {
      // This function was function-called and value is a symbol
      return SymbolDescriptiveString(value);
    }
    s = ToString(value);
  }
  if (new.target === undefined) {
    // This function was function-called
    return s;
  }
  // This function was new-called
  return StringCreate(s, new.target.prototype); // simplified!
}

String() works differently, depending on whether it is invoked via a function call or via new. It uses new.target to distinguish the two.

These are the helper functions StringCreate() and SymbolDescriptiveString():

/** 
 * Creates a String instance that wraps `value`
 * and has the given protoype.
 */
function StringCreate(value, prototype) {
  // ···
}

function SymbolDescriptiveString(sym) {
  assert.equal(TypeOf(sym), 'symbol');
  let desc = sym.description;
  if (desc === undefined) {
    desc = '';
  }
  assert.equal(TypeOf(desc), 'string');
  return 'Symbol('+desc+')';
}
2.4.2.2 Symbol.prototype.toString()

In addition to String(), we can also use method .toString() to convert a symbol to a string. Its specification looks as follows.

Symbol.prototype.toString = function () {
  let sym = thisSymbolValue(this);
  return SymbolDescriptiveString(sym);
};
function thisSymbolValue(value) {
  if (TypeOf(value) === 'symbol') {
    return value;
  }
  if (TypeOf(value) === 'object' && '__SymbolData__' in value) {
    let s = value.__SymbolData__;
    assert.equal(TypeOf(s), 'symbol');
    return s;
  }
}
2.4.2.3 Object.prototype.toString

The default specification for .toString() looks as follows:

Object.prototype.toString = function () {
  if (this === undefined) {
    return '[object Undefined]';
  }
  if (this === null) {
    return '[object Null]';
  }
  let O = ToObject(this);
  let isArray = Array.isArray(O);
  let builtinTag;
  if (isArray) {
    builtinTag = 'Array';
  } else if ('__ParameterMap__' in O) {
    builtinTag = 'Arguments';
  } else if ('__Call__' in O) {
    builtinTag = 'Function';
  } else if ('__ErrorData__' in O) {
    builtinTag = 'Error';
  } else if ('__BooleanData__' in O) {
    builtinTag = 'Boolean';
  } else if ('__NumberData__' in O) {
    builtinTag = 'Number';
  } else if ('__StringData__' in O) {
    builtinTag = 'String';
  } else if ('__DateValue__' in O) {
    builtinTag = 'Date';
  } else if ('__RegExpMatcher__' in O) {
    builtinTag = 'RegExp';
  } else {
    builtinTag = 'Object';
  }
  let tag = O[Symbol.toStringTag];
  if (TypeOf(tag) !== 'string') {
    tag = builtinTag;
  }
  return '[object ' + tag + ']';
};

This operation is used if we convert plain objects to strings:

> String({})
'[object Object]'

By default, it is also used if we convert instances of classes to strings:

class MyClass {}
assert.equal(
  String(new MyClass()), '[object Object]');

Normally, we would override .toString() in order to configure the string representation of MyClass, but we can also change what comes after “object” inside the string with the square brackets:

class MyClass {}
MyClass.prototype[Symbol.toStringTag] = 'Custom!';
assert.equal(
  String(new MyClass()), '[object Custom!]');

It is interesting to compare the overriding versions of .toString() with the original version in Object.prototype:

> ['a', 'b'].toString()
'a,b'
> Object.prototype.toString.call(['a', 'b'])
'[object Array]'

> /^abc$/.toString()
'/^abc$/'
> Object.prototype.toString.call(/^abc$/)
'[object RegExp]'

2.4.3 ToPropertyKey()

ToPropertyKey() is used by, among others, the bracket operator. This is how it works:

function ToPropertyKey(argument) {
  let key = ToPrimitive(argument, 'string'); // (A)
  if (TypeOf(key) === 'symbol') {
    return key;
  }
  return ToString(key);
}

Once again, objects are converted to primitives before working with primitives.

ToNumeric() is used by, among others, by the multiplication operator (*). This is how it works:

function ToNumeric(value) {
  let primValue = ToPrimitive(value, 'number');
  if (TypeOf(primValue) === 'bigint') {
    return primValue;
  }
  return ToNumber(primValue);
}
2.4.4.1 ToNumber()

ToNumber() works as follows:

function ToNumber(argument) {
  if (argument === undefined) {
    return NaN;
  } else if (argument === null) {
    return +0;
  } else if (argument === true) {
    return 1;
  } else if (argument === false) {
    return +0;
  } else if (TypeOf(argument) === 'number') {
    return argument;
  } else if (TypeOf(argument) === 'string') {
    return parseTheString(argument); // not shown here
  } else if (TypeOf(argument) === 'symbol') {
    throw new TypeError();
  } else if (TypeOf(argument) === 'bigint') {
    throw new TypeError();
  } else {
    // argument is an object
    let primValue = ToPrimitive(argument, 'number');
    return ToNumber(primValue);
  }
}

The structure of ToNumber() is similar to the structure of ToString().

2.5 Operations that coerce

2.5.1 Addition operator (+)

This is how JavaScript’s addition operator is specified:

function Addition(leftHandSide, rightHandSide) {
  let lprim = ToPrimitive(leftHandSide);
  let rprim = ToPrimitive(rightHandSide);
  if (TypeOf(lprim) === 'string' || TypeOf(rprim) === 'string') { // (A)
    return ToString(lprim) + ToString(rprim);
  }
  let lnum = ToNumeric(lprim);
  let rnum = ToNumeric(rprim);
  if (TypeOf(lnum) !== TypeOf(rnum)) {
    throw new TypeError();
  }
  let T = Type(lnum);
  return T.add(lnum, rnum); // (B)
}

Steps of this algorithm:

2.5.2 Abstract Equality Comparison (==)

/** Loose equality (==) */
function abstractEqualityComparison(x, y) {
  if (TypeOf(x) === TypeOf(y)) {
    // Use strict equality (===)
    return strictEqualityComparison(x, y);
  }

  // Comparing null with undefined
  if (x === null && y === undefined) {
    return true;
  }
  if (x === undefined && y === null) {
    return true;
  }

  // Comparing a number and a string
  if (TypeOf(x) === 'number' && TypeOf(y) === 'string') {
    return abstractEqualityComparison(x, Number(y));
  }
  if (TypeOf(x) === 'string' && TypeOf(y) === 'number') {
    return abstractEqualityComparison(Number(x), y);
  }

  // Comparing a bigint and a string
  if (TypeOf(x) === 'bigint' && TypeOf(y) === 'string') {
    let n = StringToBigInt(y);
    if (Number.isNaN(n)) {
      return false;
    }
    return abstractEqualityComparison(x, n);
  }
  if (TypeOf(x) === 'string' && TypeOf(y) === 'bigint') {
    return abstractEqualityComparison(y, x);
  }

  // Comparing a boolean with a non-boolean
  if (TypeOf(x) === 'boolean') {
    return abstractEqualityComparison(Number(x), y);
  }
  if (TypeOf(y) === 'boolean') {
    return abstractEqualityComparison(x, Number(y));
  }

  // Comparing an object with a primitive
  // (other than undefined, null, a boolean)
  if (['string', 'number', 'bigint', 'symbol'].includes(TypeOf(x))
    && TypeOf(y) === 'object') {
      return abstractEqualityComparison(x, ToPrimitive(y));
    }
  if (TypeOf(x) === 'object'
    && ['string', 'number', 'bigint', 'symbol'].includes(TypeOf(y))) {
      return abstractEqualityComparison(ToPrimitive(x), y);
    }
  
  // Comparing a bigint with a number
  if ((TypeOf(x) === 'bigint' && TypeOf(y) === 'number')
    || (TypeOf(x) === 'number' && TypeOf(y) === 'bigint')) {
      if ([NaN, +Infinity, -Infinity].includes(x)
        || [NaN, +Infinity, -Infinity].includes(y)) {
          return false;
        }
      if (isSameMathematicalValue(x, y)) {
        return true;
      } else {
        return false;
      }
    }
  
  return false;
}

The following operations are not shown here:

Now that we have taken a closer look at how JavaScript’s type coercion works, let’s conclude with a brief glossary of terms related to type conversion:

[Source: Wikipedia]