GUIDE · JavaScript

Equality & coercion gotchas

lesson9 sectionsruns in browser95 asserts138 lines

At a glance

RULE
always ===. The one OK ==: x == null (catches null AND undefined).
FALSY
false, 0, -0, 0n, '', null, undefined, NaN. EVERYTHING else is truthy ([] and {} and '0' and 'false' are truthy).
TYPEOF
'undefined' 'boolean' 'number' 'bigint' 'string' 'symbol' 'function' 'object' (incl. null and arrays — use Array.isArray, x === null)
COERCE
+ with a string concatenates; other arithmetic ops -> Number; == does type juggling; template literals call toString
SAY
strict vs loose equalitytype coerciontruthy/falsyObject.is for NaN and -0SameValueZero (includes / Map / Set)

The idea#

Think of === as a coat check that returns your coat only when the ticket matches exactly. The number 1 and the string '1' are a coat and a claim stub. 1 === '1' is false. Object.is is a stricter hook for two weird coats: NaN matches NaN, and 0 is different from -0. === does the opposite on both.

== is the attendant who squints and converts. The string '1' becomes the number 1, and then they say yes. That conversion is type coercion. null == undefined is true, and null == 0 is false, because null only loosely matches undefined. 0 == '' and 0 == '0' are true, and '' == '0' is false. Loose equality is not transitive. false == '0' is true. false == 'false' is false, because that string becomes NaN. Arrays go through toString, so [] == false and [1, 2] == '1,2'. An object becomes '[object Object]'.

The one loose check worth using is x == null. It catches null and undefined, and it leaves 0 alone.

Falsy values are the empty hangers: false, 0, -0, 0n, '', null, undefined, and NaN. Everything else is truthy. An empty array is still a coat. So is {}. The strings '0' and 'false' are non-empty, so they are truthy.

typeof reports a string: 'undefined', 'boolean', 'number', 'bigint', 'string', 'symbol', 'function', or 'object'. The historic misfile is typeof null === 'object'. Arrays are objects too. Use Array.isArray, and use === null for null. typeof on a name that was never declared is the safe 'undefined'. Object.prototype.toString.call(value) is the reliable tag, such as [object Null] or [object Array].

Why it matters#

Use === in real code. + next to a string glues, so 1 + '2' is '12'. Other math converts. || and && hand back an operand, and ?? only fills in for null or undefined, so 0 ?? 5 stays 0. Two empty arrays are two coats: matching is by identity. includes can find NaN where indexOf cannot. Default sort is text order, and map(parseInt) feeds the index in as the radix.

Say strict versus loose equality, type coercion, truthy and falsy, Object.is for NaN and -0, and SameValueZero.

A small example#

JS
const sameType = 1 === '1';const coerced = 1 == '1';const emptyList = [] ? 'yes' : 'no';const glued = 1 + '2';const subtracted = '3' - 1;const nan = NaN === NaN;const nanIs = Object.is(NaN, NaN);

sameType is false and coerced is true. emptyList is 'yes', because an empty array is a real object. glued is '12' and subtracted is 2. nan is false and nanIs is true.

What the code below is doing#

The first block is ===, ==, and Object.is: the conversions above, the non-transitive trio of 0, '', and '0', NaN === NaN false, Object.is(NaN, NaN) true, Object.is(0, -0) false, and 0 === -0 true. includes and Map find NaN. indexOf does not.

Two fresh arrays are not equal, two fresh objects are not equal, 'a' === 'a' is true, and one array compared to itself is true.

typeof reports null and arrays as 'object', functions as 'function', an undeclared name as 'undefined', NaN as 'number', bigint, symbol, and a class as 'function'. An array is an instanceof Array and Object. toString.call tags null, arrays, and dates.

The falsy list is those eight empty hangers. The truthy list includes [], {}, '0', 'false', and new Boolean(false). filter(Boolean) keeps 1 and 'a'.

+ concatenates. The other operators coerce to numbers: true + 1, [] + [] as '', [] + {} as '[object Object]', 1 + null as 1, 1 + undefined as NaN, and '5' + 3 - 2 as 51. +'' and +[] are 0. +{} is NaN. String(null) is 'null', while String([null]) is empty, because null inside an array becomes ''.

'10' < '9' is lexicographic. 10 < '9' is numeric. null >= 0 is true because a relational compare treats null as 0, and null == 0 is still false. Both undefined < 1 and undefined > 1 are false.

0 || 'default', '' && 'x', and 'a' && 'b' return an operand. null ?? 0 is 0. 0 ?? 5 is 0.

The trivia block is the same coat check in costume. 0.1 + 0.2 === 0.3 is false. Past Number.MAX_SAFE_INTEGER, adding 1 and adding 2 can look equal. typeof typeof 1 is 'string'. Default sort turns [10, 1, 3] into 1,10,3. ['1','7','11'].map(parseInt) is 1, NaN, 3. map(Number) is the fix. arguments is an object. void 0 is undefined. 1 < 2 < 3 is true and 3 > 2 > 1 is false. 'b' + 'a' + +'a' + 'a' is 'baNaNa'. Math.max() with no arguments is less than Math.min(). [] + null + 1 is 'null1'. A holey array [,,] has length 2. new Array(3).join('-') is '--'. Setting length truncates.

The patterns to keep: v == null for nil, an object check that rejects null and arrays, and a numeric check that requires a finite number, so the string '1' does not pass.

Everything below is the demonstration. Same idea, written as code you can run.

JSjavascript/equality-coercion-gotchas.js · setup
'use strict';const assert = require('node:assert/strict');
01

=== vs == vs Object.is#

Verified by 21 asserts
JS
assert.equal(1 === '1', false);assert.equal(1 == '1', true);                                // coercesassert.equal(null == undefined, true);assert.equal(null === undefined, false);assert.equal(null == 0, false);                              // null only == undefinedassert.equal(0 == '', true);assert.equal(0 == '0', true);assert.equal('' == '0', false);                              // == is not transitiveassert.equal(false == 'false', false);                       // 'false' -> NaNassert.equal(false == '0', true);assert.equal([] == false, true);                             // [] -> '' -> 0assert.equal([1] == 1, true);assert.equal([1, 2] == '1,2', true);assert.equal({} == '[object Object]', true);assert.equal(NaN === NaN, false);assert.equal(Object.is(NaN, NaN), true);assert.equal(Object.is(0, -0), false);assert.equal(0 === -0, true);assert.equal([NaN].includes(NaN), true);                     // SameValueZeroassert.equal([NaN].indexOf(NaN), -1);                        // strict equalityassert.equal(new Map([[NaN, 'x']]).get(NaN), 'x');
02

Reference vs value#

JS
const arr = [1];
Verified by 4 asserts
JS
assert.equal([] === [], false);assert.equal({} === {}, false);assert.equal('a' === 'a', true);                             // primitives by valueassert.equal(arr === arr, true);
03

typeof / instanceof / Array.isArray#

Verified by 14 asserts
JS
assert.equal(typeof null, 'object');                         // historic bugassert.equal(typeof [], 'object');assert.equal(typeof (() => {}), 'function');assert.equal(typeof undefined, 'undefined');assert.equal(typeof notDeclaredAnywhere, 'undefined');       // typeof is safe on undeclared identifiersassert.equal(typeof NaN, 'number');assert.equal(typeof 1n, 'bigint');assert.equal(typeof Symbol(), 'symbol');assert.equal(typeof class {}, 'function');assert.equal(Array.isArray([]), true);assert.equal([] instanceof Array && [] instanceof Object, true);assert.equal(Object.prototype.toString.call(null), '[object Null]');      // the reliable type tagassert.equal(Object.prototype.toString.call([]), '[object Array]');assert.equal(Object.prototype.toString.call(new Date(0)), '[object Date]');
04

Truthy / falsy#

JS
const falsy = [false, 0, -0, 0n, '', null, undefined, NaN];const truthy = [[], {}, '0', 'false', ' ', -1, Infinity, () => {}, new Boolean(false)];
Verified by 6 asserts
JS
assert.equal(falsy.some(Boolean), false);assert.equal(truthy.every(Boolean), true);assert.equal(!!'0', true);assert.equal(Boolean([]), true);assert.equal([] ? 'yes' : 'no', 'yes');assert.deepEqual([0, 1, '', 'a', null].filter(Boolean), [1, 'a']);        // compact idiom
05

+ and arithmetic coercion#

Verified by 17 asserts
JS
assert.equal(1 + '2', '12');assert.equal('3' - 1, 2);assert.equal('3' * '4', 12);assert.equal(true + 1, 2);assert.equal([] + [], '');assert.equal([] + {}, '[object Object]');assert.equal(1 + null, 1);assert.equal(Number.isNaN(1 + undefined), true);assert.equal('5' + 3 - 2, 51);                               // left to right: '53' - 2assert.equal(+'', 0);assert.equal(+[], 0);assert.equal(Number.isNaN(+{}), true);assert.equal(`${[1, 2]}`, '1,2');assert.equal(`${{}}`, '[object Object]');assert.equal(String(null), 'null');assert.equal(String([null]), '');                            // null/undefined become '' inside arraysassert.equal([1, [2, [3]]].toString(), '1,2,3');
06

Comparison coercion#

Verified by 7 asserts
JS
assert.equal('10' < '9', true);                              // both strings -> lexicographicassert.equal(10 < '9', false);                               // mixed -> numericassert.equal('a' < 'b', true);assert.equal(null >= 0, true);                               // null -> 0 for relational...assert.equal(null == 0, false);                              // ...but not for ==assert.equal(undefined < 1, false);                          // undefined -> NaN -> all falseassert.equal(undefined > 1, false);
07

Short-circuit returns the OPERAND, not a boolean#

Verified by 5 asserts
JS
assert.equal(0 || 'default', 'default');assert.equal('' && 'x', '');assert.equal('a' && 'b', 'b');assert.equal(null ?? 0, 0);assert.equal(0 ?? 5, 0);
08

Classic trivia#

JS
let a1 = [1, 2, 3]; a1.length = 1;                           // setting length truncates
Verified by 18 asserts
JS
assert.equal(0.1 + 0.2 === 0.3, false);assert.equal(9007199254740992 === 9007199254740993, true);assert.equal(Number.MAX_SAFE_INTEGER + 2 === Number.MAX_SAFE_INTEGER + 1, true);assert.equal(typeof typeof 1, 'string');assert.equal([10, 1, 3].sort().join(), '1,10,3');assert.equal(['1', '7', '11'].map(parseInt).join(), '1,NaN,3');   // parseInt(x, index) — the famous oneassert.deepEqual(['1', '7', '11'].map(Number), [1, 7, 11]);assert.equal((function () { return typeof arguments; })(), 'object');assert.equal(void 0, undefined);assert.equal(1 < 2 < 3, true);assert.equal(3 > 2 > 1, false);                              // (3 > 2) -> true -> 1; 1 > 1 falseassert.equal('b' + 'a' + +'a' + 'a', 'baNaNa');assert.equal(Math.max() < Math.min(), true);assert.equal([] + null + 1, 'null1');assert.equal([, ,].length, 2);                               // trailing comma doesn't countassert.equal(new Array(3).join('-'), '--');assert.equal(Array(3).toString(), ',,');assert.deepEqual(a1, [1]);
09

Safe patterns to actually use#

JS
const isNil = v => v == null;                                // the one approved ==const isObject = v => v !== null && typeof v === 'object' && !Array.isArray(v);const isNumeric = v => typeof v === 'number' && Number.isFinite(v);
Verified by 3 asserts
JS
assert.equal(isNil(undefined) && isNil(null) && !isNil(0), true);assert.equal(isObject({}) && !isObject([]) && !isObject(null), true);assert.equal(isNumeric(1) && !isNumeric(NaN) && !isNumeric('1'), true);

See also