// ───────────────────────────────────────────────────────────────────────────── // JS EQUALITY, COERCION, TRUTHINESS — "what does this print?" trivia // ───────────────────────────────────────────────────────────────────────────── // 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 equality", "type coercion", "truthy/falsy", // "Object.is for NaN and -0", "SameValueZero (includes / Map / Set)" // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── 1. === vs == vs Object.is assert.equal(1 === '1', false); assert.equal(1 == '1', true); // coerces assert.equal(null == undefined, true); assert.equal(null === undefined, false); assert.equal(null == 0, false); // null only == undefined assert.equal(0 == '', true); assert.equal(0 == '0', true); assert.equal('' == '0', false); // == is not transitive assert.equal(false == 'false', false); // 'false' -> NaN assert.equal(false == '0', true); assert.equal([] == false, true); // [] -> '' -> 0 assert.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); // SameValueZero assert.equal([NaN].indexOf(NaN), -1); // strict equality assert.equal(new Map([[NaN, 'x']]).get(NaN), 'x'); // ── 2. Reference vs value assert.equal([] === [], false); assert.equal({} === {}, false); assert.equal('a' === 'a', true); // primitives by value const arr = [1]; assert.equal(arr === arr, true); // ── 3. typeof / instanceof / Array.isArray assert.equal(typeof null, 'object'); // historic bug assert.equal(typeof [], 'object'); assert.equal(typeof (() => {}), 'function'); assert.equal(typeof undefined, 'undefined'); assert.equal(typeof notDeclaredAnywhere, 'undefined'); // typeof is safe on undeclared identifiers assert.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 tag assert.equal(Object.prototype.toString.call([]), '[object Array]'); assert.equal(Object.prototype.toString.call(new Date(0)), '[object Date]'); // ── 4. Truthy / falsy const falsy = [false, 0, -0, 0n, '', null, undefined, NaN]; assert.equal(falsy.some(Boolean), false); const truthy = [[], {}, '0', 'false', ' ', -1, Infinity, () => {}, new 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 // ── 5. + and arithmetic coercion 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' - 2 assert.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 arrays assert.equal([1, [2, [3]]].toString(), '1,2,3'); // ── 6. Comparison coercion assert.equal('10' < '9', true); // both strings -> lexicographic assert.equal(10 < '9', false); // mixed -> numeric assert.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 false assert.equal(undefined > 1, false); // ── 7. Short-circuit returns the OPERAND, not a boolean assert.equal(0 || 'default', 'default'); assert.equal('' && 'x', ''); assert.equal('a' && 'b', 'b'); assert.equal(null ?? 0, 0); assert.equal(0 ?? 5, 0); // ── 8. Classic trivia 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 one assert.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 false assert.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 count assert.equal(new Array(3).join('-'), '--'); assert.equal(Array(3).toString(), ',,'); let a1 = [1, 2, 3]; a1.length = 1; // setting length truncates assert.deepEqual(a1, [1]); // ── 9. Safe patterns to actually use 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); 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); console.log('ok: javascript/equality-coercion-gotchas.js');