How Do the JavaScript Relational Comparison Operators Coerce Types

How do the JavaScript relational comparison operators coerce types?

JavaScript relational comparison operator type coercion is defined in the JavaScript specification, specifically in sections 11.8 to 11.8.5 which describe the operators, and sections 9.1 (ToPrimitive) and 9.3 (ToNumber) which describe the process of coercing the operands.

In short, the 4 comparison operators (<, >, <=, and >=) do their best to convert each operand to a number, then compare the numbers. The exception is when both operands are strings, in which case they are compared alphabetically.

Specifically,

  1. If an argument o is an object instead of a primitive, try to convert it to a primitive value by calling o.valueOf() or - if o.valueOf wasn't defined or didn't return a primitive type when called - by calling o.toString()

  2. If both arguments are Strings, compare them according to their lexicographical ordering. For example, this means "a" < "b" and "a" < "aa" both return true.

  3. Otherwise, convert each primitive to a number, which means:

    • undefined -> NaN
    • Null -> +0
    • Boolean primitive type -> 1 if true, +0 if false
    • String -> try to parse a number from the string
  4. Then compare each item as you'd expect for the operator, with the caveat that any comparison involving NaN evaluates to false.

So, this means the following:

console.log(true > null);           //prints true
console.log(true > false); //prints true
console.log("1000.0" > 999); //prints true
console.log(" 1000\t\n" < 1001); //prints true

var oVal1 = { valueOf: function() { return 1; } };
var oVal0 = { toString: function() { return "0"; } };

console.log(oVal1 > null); //prints true
console.log(oVal0 < true); //prints true
console.log(oVal0 < oVal1); //prints true

Coercion of a number and a string in comparison operators

The coercion is in the other direction. 'v' is coerced to number, yielding NaN, which will make any comparison with another number return false.

See "What is the rationale for all comparisons returning false for IEEE754 NaN values?" on the behaviour of NaN

More details

From the EcmaScript specification:

In 12.9.3 Runtime Semantics: Evaluation the evaluation of both < and > operators is specified, with this important step:


  1. Let r be the result of performing Abstract Relational Comparison rval < lval with LeftFirst equal to false.

And,


  1. If r is undefined, return false. Otherwise, return r.

The 7.2.11 Abstract Relational Comparison starts out with:

The comparison x < y, where x and y are values, produces true, false, or undefined (which indicates that at least one operand is NaN).

NB: Note that undefined will lead to a false in the final evaluation, as stated in above quoted step 8 of section 12.9.3.

It is then required that after the primitive values have been taken from the operands, and they are not found to be both strings, they should be coerced to Number:


  1. If both px and py are Strings, then

    [...]
  2. Else

    a. Let nx be ToNumber(px).

    [...]

    c. Let ny be ToNumber(py).

Examples of Evaluated Expressions

Here is a series of comparisons, showing the different outcomes you can get:

function test(value, name) {   if (arguments.length === 1) name = JSON.stringify(value);   console.log(name + ' < 11.5 === ' + (value < 11.5) +         '. Number(' + name + ') = ', Number(value));}test('33');test('3');test('+11.9');     // coerces to number 11.9 (sign and decimal)test('0xA');       // coerces to number 10 (hexadecimal notation)test('');          // coerces to number 0test('3v');        // invalid numbertest('3e2');       // coerces to number 300 (exponent notation)test('-Infinity'); // coerces to number -Infinitytest(new Date(), 'new Date()'); // coerces to number of milliseconds test({ valueOf: x => 2 }, '{ valueOf: x => 2 }'); // coerces to number 2
.as-console-wrapper { max-height: 100% !important; top: 0; }

Behavior of Greater than (and another inequality comparison operators) on arrays

To understand the results of your test cases, you need to understand what happens when you compare arrays or any other object using any relational operator.

Long story short, objects are converted to strings before they are compared using any of the relational operator.

(skip to the end of the answer if you don't want to read about object-to-primitive conversion.)

ToPrimitive abstract operation

To convert objects to primitive values, javascript performs toPrimitive abstract operation that takes two arguments:

  • input: object that should be converted to a primitive value
  • preferredType: optional second argument that specifies the type that should be favoured when converting an object to a primitive value

For object to primitive conversion, toPrimitive abstract operation invokes another abstract operation known as OrdinaryToPrimitive

OrdinaryToPrimitive abstract operation

For object to primitive conversion, toPrimitive abstract operation invokes OrdinaryToPrimitive abstract operation with two arguments:

  • O: object that should be converted to a primitive value
  • hint: type that should be favored when converting the object to a primitive value

toPrimitive abstract operation sets the hint as following:

  • If the preferredType is a string, set hint to string
  • If the preferredType is a number, set hint to number
  • If preferredType is not specified, set hint to number

OrdinaryToPrimitive abstract operation uses following three algorithms to convert the object to a primitive value:

  • prefer-string: If hint is string, return a primitive value, preferring a string value, if conversion to string is possible

  • prefer-number: If hint is number, return a primitive value, preferring a number value, if conversion to number is possible

  • no-preference: This algorithm expresses no preference about what type of primitive value should be returned and lets the objects define what type of primitive value should be returned. If hint is default or there is no hint, this algorithm is used to convert an object to a primitive value.

    It allows objects to override the default ToPrimitive behavior. Among the built-in objects, only Date and Symbol objects override the default ToPrimitive behavior. Date and Symbol objects implement this algorithm as prefer-string whereas all the other built-in objects implement this algorithm as prefer-number (Objects can override the default ToPrimitive behavior by implementing Symbol.toPrimitive method.)

All objects inherit two methods that are used to convert objects to primitive values. These two methods are:

  • .valueOf()
  • .toString()

object to primitive conversion involves calling above mentioned methods and the object to primitive conversion algorithms mentioned above, call these two methods in different order.

prefer-string

This algorithm first calls the .toString() method and if the resulting value is a primitive value, javascript uses the returned primitive value, even if it's not a string.

If the .toString() method doesn't exists or it returns an object, then .valueOf() method is called. If .valueOf() method returns a primitive value, then that value is used otherwise TypeError is thrown.

prefer-number

Only difference between this algorithm and prefer-string is that it first invokes .valueOf() method and then .toString() method.

no-preference

When there is no preferred type or hint or if the preferred type is default, by default, prefer-number algorithm is used.

Objects can override this behaviour and of all the built-in objects, only Date and Symbol override this default ToPrimitive conversion behaviour. Date and Symbol use prefer-string algorithm when there is no preferred type or a hint or the preferred type is default.


Now coming back to you question, relational operators, i.e. <, >=, <, <= can be used to compare strings as well as numbers. If either operand of these operators is an object, it is converted to primitive value using prefer-number algorithm. So when you compare two arrays using a relational operator, javascript tries to convert each array into a primitive value using prefer-number algorithm.

As mentioned above, prefer-number algorithm first calls .valueOf() method. If return value is a primitive value, that value is used, otherwise .toString() method is called.

Default implementation of .valueOf() method simply returns the object itself rather than returning a primitive value, so javascript always ends up calling .toString() method when it uses prefer-number algorithm.

When .toValue() method is called on an array, it simply returns the array on which this method was called. Javascript then calls .toString() method on this returned array. When .toString() method is called on an array, it converts all the elements of the array into strings and then joins all the strings together with commas in between each string.

So when you compare [1] > [2], you are comparing '1' > '2' and similarly [1,2] > [1,1] is converted to '1,2' > '1,1'.

As strings are compared by their unicode code points, '1' > '2' evaluates to false and '1,2' > '1,1' evaluates to true.

The Abstract Relational Comparison Algorithm: Why evaluation order is important?

The Abstract Relational Comparison Algorithm evaluates x < y, but it is used for several operators, E.G. x < y, x > y, x >= y, sometimes by flipping the order of the operands. In the case of x > y, the spec for the Greater-than operator says:

Let r be the result of performing abstract relational comparison rval < lval with LeftFirst equal to false.

LeftFirst doesn't matter for primitives because there are no side-effects when they are coerced to numbers for the comparison. However, for objects there can be side-effects:

const x = { valueOf: _ => console.log( 'x' ) };const y = { valueOf: _ => console.log( 'y' ) };
y > x;

null and undefined inconsistent comparison

tl;dr The >= ends up coercing both arguments to numbers in this case: undefined gets coerced to NaN while null gets coerced to 0, which aren't equal. For ==, the spec explicitly defines that null == undefined is true.


The values do, in fact, get coerced in both cases (in a sense, at least - the case with == is special). Let's consider them one at a time, with the help of the spec.

The algorithm for the >= operator uses the "Abstract Relational Comparison Algorithm", which is shared by other relational operators. From the description in the spec, we see that the algorithm does the following:

  1. Converts the arguments to primitives (which null and undefined already are).
  2. Checks if the arguments are Strings (which they are not).
  3. If they are not Strings, the algorithm converts the arguments to numbers (see steps 3.a. and 3.b.) and performs the comparison with the results.

The last point is the key. From the ToNumber table, we see that undefined gets coerced to NaN, and the algorithm considers any comparison with NaN as being falsy (see steps 3.c. and 3.d.). Thus, null >= undefined is false.


For the other case, ==, the story is actually much simpler: the spec explicitly states that null == undefined is true as part of the "Abstract Equality Comparison Algorithm" (see steps 2. and 3.). Thus, null == undefined is true.

JavaScript less than or equals (bool = int)

The representation of false in memory is 0. That's why you obtain those results.

jQuery/Javascript the comparison operators === and ==

In Javascript, === do not try to coerce the type of the variables you are testing, while == will do its best to 'typecast' those variables if needed to compare them.

For instance 1 == '1' returns true, while 1 === '1' returns false since you are comparing a number to a string.

Lastly, jQuery and pure javascript both uses the same === and == operators. Hence, there will not be any difference between the two.

The MDN documentation is pretty good too.

Allow nullable operands in less- or greater-than comparisons in TypeScript

nonNumber < number replies on javascript type coercion to work (converts null to a number when compared to a number). And type coercion is which is widely regarded as something to avoid in all javascript programs due to non obvious ways that values are transformed.

Typescript is assuming that more often than not, a nullable number used in a comparison is probably a potential bug. It's likely the programmer just forgot that the variable could be null when writing that comparison.

So what typescript wants is for you to explicitly handle the null case. This tells readers of your code exactly what happens if that value is null and that the original programmer intended null to be handled precisely that way.

Something like this perhaps?

const val = f()
let b = val !== null && val < 0

or:

let b = (f() ?? 0) < 0

You could use f()! as you note, and that should work to silence the error. However, the ! is intended to say "the programmer is asserting that this value is not null here, even if the compiler thinks it might be". Which isn't quite accurate, as you already noted.

Why doesn't JavaScript have strict greater/less than comparison operators?

I can only guess-

If

a === b is false, then

a !== b is true. always.

But, this implication wouldn't hold for <==

If

x <== 20 is false, we cannot infer the result of
x >== 20 because it might have been false due to type check, or the relation check.

I think that's slightly confusing, although there's plenty of things in the language that are much worse (type coercion in general, to name one).

However, I think a strict < or > would behave consistently.



Related Topics



Leave a reply



Submit