Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save carefree-ladka/0662f3bd1a5163d51bb2d14ac1bce590 to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/0662f3bd1a5163d51bb2d14ac1bce590 to your computer and use it in GitHub Desktop.
JavaScript Type Coercion: From Basic to Advanced

JavaScript Type Coercion: From Basic to Advanced

A complete guide to understanding how JavaScript silently converts types — and how to master it.


Table of Contents

  1. What is Type Coercion?
  2. JavaScript's Type System
  3. Implicit vs Explicit Coercion
  4. String Coercion
  5. Number Coercion
  6. Boolean Coercion
  7. The + Operator — The Great Deceiver
  8. Loose Equality (==) vs Strict Equality (===)
  9. Abstract Equality Comparison Algorithm
  10. Object-to-Primitive Coercion
  11. Relational Operators (<, >, <=, >=)
  12. Logical Operators and Short-Circuit Evaluation
  13. Nullish Coalescing (??)
  14. Template Literals and Coercion
  15. Array Coercion Edge Cases
  16. Symbol and BigInt Coercion
  17. Common Gotchas and Bugs
  18. Best Practices
  19. Quick Reference Cheat Sheet

1. What is Type Coercion?

Type coercion is the automatic or implicit conversion of values from one data type to another. JavaScript is a dynamically typed language, meaning variables don't have fixed types — and the engine will happily convert types behind the scenes when operations require it.

// You wrote this:
"5" + 3

// JavaScript did this silently:
"5" + String(3) // → "53"

This behavior can be a superpower when understood — or a source of notorious bugs when not.


2. JavaScript's Type System

JavaScript has 8 data types:

Type Example Primitive?
undefined undefined ✅ Yes
null null ✅ Yes
boolean true, false ✅ Yes
number 42, 3.14, NaN, Infinity ✅ Yes
bigint 9007199254740991n ✅ Yes
string "hello" ✅ Yes
symbol Symbol("id") ✅ Yes
object {}, [], null ❌ No

Note: typeof null === "object" is a historical JavaScript bug — null is actually a primitive.

typeof undefined  // "undefined"
typeof true       // "boolean"
typeof 42         // "number"
typeof "hello"    // "string"
typeof Symbol()   // "symbol"
typeof 42n        // "bigint"
typeof {}         // "object"
typeof []         // "object"
typeof null       // "object"  ← the famous bug
typeof function(){} // "function"

3. Implicit vs Explicit Coercion

Explicit Coercion (Type Casting)

You intentionally convert a type using built-in functions.

// String conversion
String(42)        // "42"
String(true)      // "true"
String(null)      // "null"
String(undefined) // "undefined"
String([1, 2, 3]) // "1,2,3"

// Number conversion
Number("42")      // 42
Number("")        // 0
Number(true)      // 1
Number(false)     // 0
Number(null)      // 0
Number(undefined) // NaN
Number("hello")   // NaN

// Boolean conversion
Boolean(0)        // false
Boolean("")       // false
Boolean(null)     // false
Boolean(undefined)// false
Boolean(NaN)      // false
Boolean("hello")  // true
Boolean(42)       // true
Boolean([])       // true  ← empty array is truthy!
Boolean({})       // true  ← empty object is truthy!

Implicit Coercion

JavaScript converts types automatically based on context.

// Implicit: JS engine converts for you
"5" * 2           // 10   (string → number)
"5" - 2           // 3    (string → number)
true + true       // 2    (boolean → number)
false + 1         // 1    (boolean → number)
null + 1          // 1    (null → 0)
undefined + 1     // NaN  (undefined → NaN)

4. String Coercion

When does string coercion happen?

  • Using the + operator when one operand is a string
  • Using template literals `${}`
  • Calling .toString() on any value
  • Passing to String()
// Explicit
String(42)          // "42"
String(null)        // "null"
String(undefined)   // "undefined"
String(true)        // "true"
String(false)       // "false"
String([1, 2, 3])   // "1,2,3"
String({})          // "[object Object]"
String(Symbol("s")) // "Symbol(s)"

// Implicit via +
42 + ""             // "42"
null + ""           // "null"
undefined + ""      // "undefined"
true + ""           // "true"
[1, 2] + ""         // "1,2"
{} + ""             // "[object Object]"

.toString() on different types

(42).toString()        // "42"
(42).toString(2)       // "101010"  — binary!
(42).toString(16)      // "2a"      — hex!
(255).toString(16)     // "ff"
true.toString()        // "true"
[1, 2, 3].toString()   // "1,2,3"
({}).toString()        // "[object Object]"

5. Number Coercion

When does number coercion happen?

  • Arithmetic operators: -, *, /, %, **
  • Unary + operator
  • Comparison operators with non-string operands
  • Number(), parseInt(), parseFloat()
// Explicit
Number("3.14")      // 3.14
Number("  42  ")    // 42    — trims whitespace!
Number("")          // 0
Number("0x1F")      // 31   — hex string
Number("0b101")     // 5    — binary string
Number("0o7")       // 7    — octal string
Number(true)        // 1
Number(false)       // 0
Number(null)        // 0
Number(undefined)   // NaN
Number([])          // 0    — empty array!
Number([1])         // 1    — single-element array!
Number([1, 2])      // NaN  — multi-element array
Number({})          // NaN
Number("abc")       // NaN

// Unary + (implicit Number())
+"42"               // 42
+""                 // 0
+true               // 1
+false              // 0
+null               // 0
+undefined          // NaN
+[]                 // 0
+[1]                // 1
+{}                 // NaN

parseInt vs parseFloat vs Number

// parseInt reads until it hits a non-digit
parseInt("42px")        // 42   ← stops at "p"
parseInt("3.14")        // 3    ← stops at "."
parseInt("0xFF", 16)    // 255  ← parse hex
parseInt("10", 2)       // 2    ← parse binary
parseInt("abc")         // NaN
parseInt("")            // NaN  ← unlike Number("") = 0

// parseFloat
parseFloat("3.14rem")   // 3.14
parseFloat("3.14.15")   // 3.14  ← stops at second dot

// Number is strict
Number("42px")          // NaN  ← can't parse "px"
Number("3.14")          // 3.14
Number("")              // 0    ← unlike parseInt("") = NaN

6. Boolean Coercion

Falsy Values — Only 8 exist in JavaScript

// These are ALL the falsy values:
Boolean(false)      // false
Boolean(0)          // false
Boolean(-0)         // false
Boolean(0n)         // false  ← BigInt zero
Boolean("")         // false
Boolean(null)       // false
Boolean(undefined)  // false
Boolean(NaN)        // false

// EVERYTHING else is truthy!
Boolean("false")    // true  ← non-empty string
Boolean("0")        // true  ← non-empty string
Boolean([])         // true  ← empty array!
Boolean({})         // true  ← empty object!
Boolean(function(){}) // true
Boolean(-1)         // true
Boolean(Infinity)   // true

When does boolean coercion happen?

// if / else
if ("") { } else { console.log("falsy") }   // "falsy"
if ([]) { console.log("truthy") }            // "truthy" — gotcha!

// Logical operators
!!"hello"           // true
!!0                 // false
!!null              // false
!![]                // true   ← empty array is truthy!

// Ternary
"" ? "yes" : "no"   // "no"
[] ? "yes" : "no"   // "yes"  ← truthy!

// while / for conditions
let arr = [1, 2, 3]
while (arr.length) {    // coerces arr.length (number) to boolean
  arr.pop()
}

7. The + Operator — The Great Deceiver

The + operator has dual behavior: addition (numbers) and concatenation (strings). This makes it the most confusing operator in JavaScript.

The Rule

If either operand is a string (or becomes a string after toPrimitive), + concatenates. Otherwise, it adds.

// Number + Number = addition
1 + 2               // 3

// String + anything = concatenation
"1" + 2             // "12"
1 + "2"             // "12"
"" + 1              // "1"
"" + true           // "true"
"" + null           // "null"
"" + undefined      // "undefined"

// Watch out for left-to-right evaluation
1 + 2 + "3"         // "33"  (1+2=3, then 3+"3"="33")
"1" + 2 + 3         // "123" ("1"+2="12", then "12"+3="123")

// null and undefined
null + null         // 0     (null→0, 0+0=0)
null + undefined    // NaN   (undefined→NaN)
undefined + undefined // NaN
null + 1            // 1     (null→0)
undefined + 1       // NaN

// boolean
true + true         // 2     (1+1)
true + false        // 1     (1+0)
true + 1            // 2
false + []          // "false" — [] converts to "", false→"false"

// Objects and Arrays
[] + []             // ""    ([]→"", ""+"" = "")
[] + {}             // "[object Object]"
{} + []             // 0  ← when {} is a block, not object!
({}) + []           // "[object Object]"

The {} + [] Mystery

// In a script / as a statement:
{} + []   // 0
// {} is parsed as an EMPTY BLOCK, not an object literal
// So it becomes: {} (block); +[] (unary plus on array)
// +[] → +("") → 0

// When forced to be an expression:
({}) + []           // "[object Object]"
var x = {} + []     // "[object Object]"
console.log({} + []) // "[object Object]"  ← expression context

8. Loose Equality (==) vs Strict Equality (===)

Strict Equality (===) — No coercion

// === never coerces. Different types = false.
1 === 1             // true
1 === "1"           // false
null === undefined  // false
NaN === NaN         // false  ← NaN is never equal to itself!

// Use Object.is() for NaN comparison
Object.is(NaN, NaN) // true
Object.is(-0, 0)    // false  ← also detects -0 vs 0

Loose Equality (==) — With coercion

// == coerces types before comparing
1 == "1"            // true   (string→number)
1 == true           // true   (true→1)
0 == false          // true   (false→0)
0 == ""             // true   (""→0)
0 == "0"            // true   ("0"→0)
"" == false         // true   (both→0)
null == undefined   // true   (special rule)
null == 0           // false  (null only == null/undefined)
null == false       // false
undefined == false  // false
NaN == NaN          // false  (NaN is never equal)

// Arrays and objects
[] == false         // true   ([]→""→0, false→0)
[] == 0             // true   ([]→""→0)
[] == ""            // true   ([]→"")
[1] == 1            // true   ([1]→"1"→1)
[[]] == 0           // true   ([[]]→[]→""→0)

The Famous Loose Equality Table

// Confusing truths:
"" == false         // true
"" == 0             // true
false == 0          // true
// But:
"" != false != 0    // they're not all equal to each other
                    // wait — actually:
"" == false == 0    // this is parsed as ("" == false) == 0
                    // → true == 0 → 1 == 0 → false!

9. Abstract Equality Comparison Algorithm

Here's the actual algorithm JavaScript uses for x == y:

1. If Type(x) === Type(y):
   - If both are NaN → false
   - Otherwise, compare values

2. If x is null and y is undefined → true
3. If x is undefined and y is null → true

4. If Type(x) is Number and Type(y) is String:
   → compare x == ToNumber(y)

5. If Type(x) is String and Type(y) is Number:
   → compare ToNumber(x) == y

6. If Type(x) is Boolean:
   → compare ToNumber(x) == y

7. If Type(y) is Boolean:
   → compare x == ToNumber(y)

8. If Type(x) is String/Number/Symbol and Type(y) is Object:
   → compare x == ToPrimitive(y)

9. If Type(x) is Object and Type(y) is String/Number/Symbol:
   → compare ToPrimitive(x) == y

10. Otherwise → false
// Tracing through: [] == false
// Step 6: false is Boolean → [] == ToNumber(false) → [] == 0
// Step 9: [] is Object → ToPrimitive([]) == 0 → "" == 0
// Step 5: "" is String → ToNumber("") == 0 → 0 == 0 → true ✅

// Tracing: null == false
// Step 2/3: null is not undefined → not matched
// Step 6: false is Boolean → null == ToNumber(false) → null == 0
// Step 10: types don't match remaining rules → false ✅

10. Object-to-Primitive Coercion

When an object needs to become a primitive, JavaScript uses the ToPrimitive abstract operation. It looks for these methods in order:

The Three Hints

// "number" hint → prefers valueOf() first
// "string" hint → prefers toString() first
// "default" hint → same as "number" for most objects

valueOf() and toString()

const obj = {
  valueOf() { return 42 },
  toString() { return "hello" }
}

+obj          // 42   (number hint → valueOf first)
`${obj}`      // "hello" (string hint → toString first... wait)
obj + ""      // "42"  (default hint → valueOf first → 42 → "42")
obj + 1       // 43   (number: valueOf → 42 + 1)

[Symbol.toPrimitive]

The most powerful override — lets you control coercion for all hints:

const temperature = {
  celsius: 25,
  [Symbol.toPrimitive](hint) {
    if (hint === "number") return this.celsius
    if (hint === "string") return `${this.celsius}°C`
    return this.celsius  // "default"
  }
}

+temperature            // 25       (number hint)
`${temperature}`        // "25°C"   (string hint)
temperature + 0         // 25       (default hint → number)
temperature + ""        // "25"     (default hint → number → string)

console.log(`Temp: ${temperature}`) // "Temp: 25°C"

Custom Objects — Practical Example

class Money {
  constructor(amount, currency) {
    this.amount = amount
    this.currency = currency
  }

  valueOf() {
    return this.amount  // Used in arithmetic
  }

  toString() {
    return `${this.amount} ${this.currency}`  // Used in string context
  }

  [Symbol.toPrimitive](hint) {
    if (hint === "string") return this.toString()
    return this.valueOf()
  }
}

const price = new Money(99.99, "USD")
const tax = new Money(8.99, "USD")

console.log(price + tax)      // 108.98       (number: valueOf)
console.log(`${price}`)       // "99.99 USD"  (string: toString)
console.log(price > 50)       // true         (comparison: valueOf)
console.log(price * 2)        // 199.98       (arithmetic: valueOf)

11. Relational Operators (<, >, <=, >=)

Relational operators follow a slightly different coercion path than ==:

Both operands are strings → lexicographic comparison

"apple" < "banana"  // true  (lexicographic)
"b" > "a"           // true
"10" > "9"          // false (lexicographic! "1" < "9")
"abc" < "abd"       // true  (compares char by char)

Otherwise → convert both to numbers

"10" > 9            // true  ("10"→10, 10>9)
"10" > "9"          // false (both strings → lexicographic: "1"<"9")
null > 0            // false (null→0, 0>0 = false)
null < 0            // false (null→0, 0<0 = false)
null == 0           // false (special rule: null only == null/undefined)
null >= 0           // true  ← SURPRISING! (null→0, 0>=0 = true)
undefined > 0       // false (undefined→NaN, NaN comparisons = false)
undefined < 0       // false
undefined == 0      // false

// String vs Number
"5" > 3             // true  ("5"→5, 5>3)
"5" < "30"          // false (lexicographic: "5" > "3")
5 > "30"            // false (5 > 30 = false)

The null >= 0 Paradox

null > 0   // false
null == 0  // false
null >= 0  // true  ← Wait, what??

// Explanation:
// > and < convert null to 0 via ToNumber
// null > 0  → 0 > 0  → false
// null >= 0 → 0 >= 0 → true
// But == uses a special rule: null only == null or undefined
// So null == 0 → false (without type conversion)

12. Logical Operators and Short-Circuit Evaluation

&& and || — They return VALUES, not booleans!

// || returns the FIRST truthy value, or the last value
"hello" || "world"    // "hello"
"" || "world"         // "world"
0 || false || "hi"    // "hi"
0 || false || null    // null   (last value if all falsy)
false || 0            // 0

// && returns the FIRST falsy value, or the last value
"hello" && "world"    // "world"
"hello" && null       // null
null && "world"       // null
1 && 2 && 3           // 3     (all truthy, returns last)
1 && 0 && 3           // 0     (returns first falsy)

Real-world Patterns

// Default values (pre-nullish coalescing)
function greet(name) {
  name = name || "Guest"  // fallback if name is falsy
  return `Hello, ${name}!`
}

greet("Alice")   // "Hello, Alice!"
greet("")        // "Hello, Guest!"  ← "" is falsy
greet(0)         // "Hello, Guest!"  ← 0 is falsy (bug if 0 is valid!)

// Guard clause with &&
const user = { name: "Alice", address: { city: "NYC" } }
const city = user && user.address && user.address.city  // "NYC"

const noAddress = { name: "Bob" }
const city2 = noAddress && noAddress.address && noAddress.address.city  // undefined

// Short-circuit evaluation (prevents execution)
const obj = null
obj && obj.doSomething()  // null — doSomething() never called

// Conditional rendering pattern (React-like)
const isLoggedIn = true
const element = isLoggedIn && "<UserDashboard />"  // "<UserDashboard />"

13. Nullish Coalescing (??)

Introduced in ES2020, ?? only falls back when the value is null or undefined — not other falsy values.

// ?? vs ||
0 || "default"      // "default"  (0 is falsy)
0 ?? "default"      // 0          (0 is not null/undefined!)

"" || "default"     // "default"  ("" is falsy)
"" ?? "default"     // ""         ("" is not null/undefined!)

false || "default"  // "default"
false ?? "default"  // false

null || "default"   // "default"
null ?? "default"   // "default"  (same here)

undefined || "default" // "default"
undefined ?? "default" // "default"  (same here)

Optional Chaining + Nullish Coalescing

const config = {
  port: 0,          // 0 is a valid port!
  debug: false,     // false is a valid setting!
  timeout: null     // explicitly no timeout
}

// Bug-prone with ||
const port = config.port || 3000        // 3000 — BUG! 0 is valid
const debug = config.debug || true      // true — BUG! false is valid

// Correct with ??
const port2 = config.port ?? 3000       // 0    ✅
const debug2 = config.debug ?? false    // false ✅
const timeout = config.timeout ?? 5000  // 5000 ✅

// Optional chaining + ??
const user = null
const city = user?.address?.city ?? "Unknown"  // "Unknown"

14. Template Literals and Coercion

Template literals always coerce values to strings using the toString() method.

const num = 42
const arr = [1, 2, 3]
const obj = { x: 1 }
const sym = Symbol("s")

`Value: ${num}`        // "Value: 42"
`Array: ${arr}`        // "Array: 1,2,3"
`Object: ${obj}`       // "Object: [object Object]"
`Null: ${null}`        // "Null: null"
`Undef: ${undefined}`  // "Undef: undefined"
`Bool: ${true}`        // "Bool: true"

// Symbol throws in template literals!
// `Sym: ${sym}`       // TypeError: Cannot convert a Symbol to a string
`Sym: ${sym.toString()}` // "Sym: Symbol(s)"  ← must be explicit

// Custom toString gives control
class Point {
  constructor(x, y) { this.x = x; this.y = y }
  toString() { return `(${this.x}, ${this.y})` }
}

const p = new Point(3, 4)
`Point is: ${p}`      // "Point is: (3, 4)"

Tagged Template Literals

function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const val = values[i - 1]
    return result + `[${val}]` + str
  })
}

const name = "Alice"
const age = 30
highlight`Name: ${name} and Age: ${age}` // "Name: [Alice] and Age: [30]"

15. Array Coercion Edge Cases

Arrays have some of the most surprising coercion behavior in JavaScript.

// Array → String (join with comma)
[].toString()         // ""
[1].toString()        // "1"
[1, 2, 3].toString()  // "1,2,3"
[[1, 2], [3]].toString() // "1,2,3"  (nested arrays flattened)

// Array → Number
Number([])            // 0   ([]→""→0)
Number([1])           // 1   ([1]→"1"→1)
Number([1, 2])        // NaN ([1,2]→"1,2"→NaN)
Number([""])          // 0   ([""]→""→0)

// The wild comparisons
[] == ![]             // true ← one of JS's most famous WTFs!
// Explanation:
// ![] → !true → false   ([] is truthy, so ![] is false)
// [] == false           → (step 6 in abstract equality)
// [] == ToNumber(false) → [] == 0
// ToPrimitive([]) == 0  → "" == 0
// ToNumber("") == 0     → 0 == 0 → true!

[] + []               // ""    (both → "")
[] + {}               // "[object Object]"
{} + []               // 0     (block + unary +)
[1] + [2]             // "12"  ("1" + "2")
[1, 2] + [3, 4]       // "1,23,4"

// Sorting without comparator — coerces to strings!
[10, 9, 2, 1, 100].sort()  // [1, 10, 100, 2, 9] ← lexicographic!
[10, 9, 2, 1, 100].sort((a, b) => a - b)  // [1, 2, 9, 10, 100] ✅

16. Symbol and BigInt Coercion

Symbol — Very Restrictive

Symbols are designed to NOT coerce silently — they throw errors.

const sym = Symbol("mySymbol")

// String coercion throws
String(sym)           // "Symbol(mySymbol)" ← explicit OK
sym.toString()        // "Symbol(mySymbol)"
`${sym}`              // TypeError!    ← implicit throws
sym + ""              // TypeError!    ← implicit throws
sym + "string"        // TypeError!

// Number coercion throws
Number(sym)           // TypeError!
+sym                  // TypeError!
sym + 1               // TypeError!

// Boolean coercion works (always true)
Boolean(sym)          // true
!!sym                 // true
if (sym) {}           // no error

BigInt — Mostly Numeric, Some Restrictions

const big = 42n

// Arithmetic with BigInt
big + 1n              // 43n  ✅
big * 2n              // 84n  ✅
big + 1               // TypeError! Cannot mix BigInt and Number

// Explicit conversion
Number(42n)           // 42   ← loses precision for huge values
BigInt(42)            // 42n
BigInt("100")         // 100n

// String coercion works
String(42n)           // "42"
`${42n}`              // "42"
42n + ""              // "42"

// Comparison (works without coercion issue)
42n == 42             // true  (loose equality with coercion)
42n === 42            // false (strict: different types)
42n > 10              // true  (relational works across types)
42n < 100             // true

// Boolean coercion
Boolean(0n)           // false
Boolean(42n)          // true
!!0n                  // false

17. Common Gotchas and Bugs

Gotcha 1: parseInt without radix

// Always specify radix — old browsers treated "0x" as hex, "0" as octal
parseInt("010")     // 8 in old JS (octal), 10 in modern JS
parseInt("010", 10) // 10 — always explicit ✅
parseInt("0x10")    // 16 — hex (this one is safe)
parseInt("010", 8)  // 8  — explicit octal

Gotcha 2: typeof NaN === "number"

typeof NaN          // "number" — NaN is technically a number type!
isNaN("hello")      // true  — coerces to NaN first!
isNaN(undefined)    // true
isNaN(null)         // false (null → 0)

// Better: use Number.isNaN (no coercion)
Number.isNaN("hello")    // false ✅
Number.isNaN(NaN)        // true  ✅
Number.isNaN(undefined)  // false ✅

// Or Object.is
Object.is(NaN, NaN)      // true  ✅

Gotcha 3: Array .includes vs indexOf

const arr = [1, NaN, null, undefined]

arr.indexOf(NaN)      // -1  ← NaN !== NaN
arr.includes(NaN)     // true ← uses Object.is internally ✅
arr.indexOf(null)     // 1   ✅
arr.includes(null)    // true ✅

Gotcha 4: Empty array in boolean context

// Empty array is TRUTHY
if ([]) console.log("truthy")  // logs "truthy"!

// But when compared to false...
[] == false    // true!

// This creates the paradox:
if ([]) {
  // This runs ([] is truthy)
}
if ([] == false) {
  // This also runs ([] == false is true)
}

Gotcha 5: + with objects on the left

{} + 1          // 1   — {} is a block statement!
({}) + 1        // "[object Object]1"
({}) - 1        // NaN — but - doesn't have this ambiguity

Gotcha 6: Comparing with null

null > 0        // false
null < 0        // false
null == 0       // false
null >= 0       // true  ← !!!
null <= 0       // true  ← !!!

// Rule: null only == null or undefined with ==
// But >= and <= convert null to 0 first

Gotcha 7: switch uses ===

const x = "1"
switch (x) {
  case 1:         // uses ===, so "1" !== 1
    console.log("number one")
    break
  case "1":       // "1" === "1" ✅
    console.log("string one")  // This runs!
    break
}

18. Best Practices

1. Prefer === over ==

// ❌ Risky
if (x == null) {}       // true for both null and undefined

// ✅ Intentional (the one valid use of ==)
if (x == null) {}       // OK if you want to catch null OR undefined

// ✅ Explicit
if (x === null || x === undefined) {}
if (x == null) {}  // shorthand for above, acceptable

2. Use Number.isNaN not isNaN

// ❌
isNaN("hello")        // true — misleading!

// ✅
Number.isNaN("hello") // false — "hello" is not NaN, it's a string
Number.isNaN(NaN)     // true

3. Use ?? instead of || for defaults

// ❌ Loses valid falsy values
const port = config.port || 3000   // wrong when port = 0

// ✅
const port = config.port ?? 3000   // correct

4. Explicit coercion over implicit

// ❌ Implicit and surprising
const total = items.length + ""
const count = +"5"

// ✅ Explicit and clear
const total = String(items.length)
const count = Number("5")
// or
const count = parseInt("5", 10)

5. Always use radix with parseInt

// ❌
parseInt("10")       // works, but intention unclear

// ✅
parseInt("10", 10)   // decimal
parseInt("ff", 16)   // hex
parseInt("101", 2)   // binary

6. Explicit sort comparator for numbers

// ❌ Sorts lexicographically
[10, 2, 100, 1].sort()

// ✅ Sorts numerically
[10, 2, 100, 1].sort((a, b) => a - b)   // ascending
[10, 2, 100, 1].sort((a, b) => b - a)   // descending

19. Quick Reference Cheat Sheet

Number() Conversion Table

Value Number() result
true 1
false 0
null 0
undefined NaN
"" 0
" " 0
"42" 42
"3.14" 3.14
"abc" NaN
"0x1F" 31
[] 0
[1] 1
[1,2] NaN
{} NaN

Boolean() — Falsy Values Only

Value Boolean() result
false false
0 false
-0 false
0n false
"" false
null false
undefined false
NaN false
everything else true

Operator Coercion Quick Reference

Expression Result Why
1 + "2" "12" String concatenation
1 - "2" -1 Numeric subtraction
true + 1 2 true1
false + 1 1 false0
null + 1 1 null0
undefined + 1 NaN undefinedNaN
[] + [] "" Both → ""
[] + {} "[object Object]" []"", {}"[object Object]"
!!"" false "" is falsy
!![] true [] is truthy
[] == false true []""0, false0
null == undefined true Special rule
null == 0 false null only equals null/undefined
NaN == NaN false NaN is never equal to itself

Golden Rule: When in doubt, be explicit. Use Number(), String(), Boolean(), ===, and ?? to make your intent clear to other developers — and to your future self.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment