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,
If an argument
o
is an object instead of a primitive, try to convert it to a primitive value by callingo.valueOf()
or - ifo.valueOf
wasn't defined or didn't return a primitive type when called - by callingo.toString()
If both arguments are Strings, compare them according to their lexicographical ordering. For example, this means
"a" < "b"
and"a" < "aa"
both return true.Otherwise, convert each primitive to a number, which means:
undefined
->NaN
Null
-> +0Boolean
primitive type ->1
iftrue
,+0
iffalse
String
-> try to parse a number from the string
Then compare each item as you'd expect for the operator, with the caveat that any comparison involving
NaN
evaluates tofalse
.
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:
- Let r be the result of performing Abstract Relational Comparison rval < lval with LeftFirst equal to
false
.
And,
- If r is
undefined
, returnfalse
. 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
, orundefined
(which indicates that at least one operand isNaN
).
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:
- If both px and py are Strings, then
[...]- 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 valuepreferredType
: 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 valuehint
: 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 astring
, sethint
tostring
- If the
preferredType
is anumber
, sethint
tonumber
- If
preferredType
is not specified, sethint
tonumber
OrdinaryToPrimitive abstract operation uses following three algorithms to convert the object to a primitive value:
prefer-string: If
hint
isstring
, return a primitive value, preferring a string value, if conversion to string is possibleprefer-number: If
hint
isnumber
, return a primitive value, preferring a number value, if conversion to number is possibleno-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
isdefault
or there is nohint
, 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, onlyDate
andSymbol
objects override the defaultToPrimitive
behavior.Date
andSymbol
objects implement this algorithm asprefer-string
whereas all the other built-in objects implement this algorithm asprefer-number
(Objects can override the defaultToPrimitive
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:
- Converts the arguments to primitives (which
null
andundefined
already are). - Checks if the arguments are
String
s (which they are not). - If they are not
String
s, 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-
Ifa === b
is false, thena !== b
is true. always.
But, this implication wouldn't hold for <==
Ifx <== 20
is false, we cannot infer the result ofx >== 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
Understanding What Goes on with Textarea Selection with JavaScript
Changing the Default Title of Confirm() in JavaScript
Adding Prototype to JavaScript Object Literal
Passing in Dynamic Key:Value Pairs to an Object Literal
How to Persist a Es6 Map in Localstorage (Or Elsewhere)
How to Format a Date in Mm/Dd/Yyyy Hh:Mm:Ss Format in JavaScript
Understanding React-Redux and Mapstatetoprops()
D3 Appending Text to a Svg Rectangle
Input File to Array JavaScript/Jquery
How to Check If a Selector Matches Something in Jquery
Object.Getownpropertynames VS Object.Keys
Why Does a Module Level Return Statement Work in Node.Js
Sort an Array with Arrays in It by String
Chrome Extension: Get Page Variables in Content Script