20 minlesson

Deep Dive: Type Coercion & Edge Cases

Deep Dive: Type Coercion & Edge Cases

This lesson explores the trickier aspects of JavaScript's type system that every developer should understand.

Explicit vs Implicit Coercion

Explicit Coercion

You intentionally convert types using built-in functions:

javascript
1// To String
2String(123) // "123"
3String(true) // "true"
4String(null) // "null"
5(123).toString() // "123"
6
7// To Number
8Number("42") // 42
9Number("42px") // NaN
10Number(true) // 1
11Number(false) // 0
12Number(null) // 0
13Number(undefined) // NaN
14parseInt("42px") // 42 (stops at non-digit)
15parseFloat("3.14m") // 3.14
16
17// To Boolean
18Boolean(0) // false
19Boolean("") // false
20Boolean("0") // true (non-empty string!)
21Boolean([]) // true (object!)
22Boolean({}) // true
23!!value // Double NOT - common idiom

Implicit Coercion

JavaScript automatically converts types in certain operations:

javascript
1// + with strings concatenates
2"5" + 3 // "53"
33 + "5" // "35"
4"" + 42 // "42"
5
6// Other math operators convert to numbers
7"6" - 2 // 4
8"6" * "2" // 12
9"6" / 2 // 3
10
11// Comparison operators
12"10" > 9 // true (string "10" converted to 10)
13"10" > "9" // false (string comparison: "1" < "9")

The Abstract Equality Algorithm (==)

When using ==, JavaScript follows specific rules:

  1. If types are the same, compare values
  2. null == undefined is true
  3. Number vs String: convert string to number
  4. Boolean vs anything: convert boolean to number first
  5. Object vs primitive: convert object via valueOf() or toString()
javascript
1// Examples
2"5" == 5 // true (string to number)
3true == 1 // true (boolean to number)
4false == 0 // true
5null == undefined // true (special case)
6null == false // false (null only equals undefined)
7[] == false // true ([] -> "" -> 0, false -> 0)
8[] == ![] // true (this is why we use ===)

Why Always Use ===

Strict equality (===) never coerces types:

javascript
1"5" === 5 // false
2true === 1 // false
3null === undefined // false
4[] === false // false

Rule: Always use === and !== unless you have a specific reason not to.

Truthy/Falsy Edge Cases

Only 6 falsy values exist in JavaScript:

javascript
1if (false) {} // falsy
2if (0) {} // falsy
3if (-0) {} // falsy (yes, -0 exists)
4if ("") {} // falsy
5if (null) {} // falsy
6if (undefined) {} // falsy
7if (NaN) {} // falsy

Everything else is truthy, including some surprises:

javascript
1if ("0") {} // truthy (non-empty string)
2if ("false") {} // truthy (non-empty string)
3if ([]) {} // truthy (object)
4if ({}) {} // truthy (object)
5if (function(){}) {} // truthy
6if (new Boolean(false)) {} // truthy (object wrapper!)

Object Wrapper Gotchas

Primitive wrappers can cause confusion:

javascript
1const str = "hello"; // primitive
2const strObj = new String("hello"); // object wrapper
3
4typeof str // "string"
5typeof strObj // "object"
6
7str == strObj // true (coercion)
8str === strObj // false (different types)
9
10// Boolean wrappers are especially dangerous
11const falseObj = new Boolean(false);
12if (falseObj) {
13 console.log("This runs!"); // Object is truthy!
14}

Rule: Never use new String(), new Number(), or new Boolean().

NaN: Not a Number

NaN is a special numeric value with unique behavior:

javascript
1// NaN is produced by invalid math
20 / 0 // NaN
3Math.sqrt(-1) // NaN
4parseInt("hello") // NaN
5undefined + 1 // NaN
6
7// NaN is not equal to anything, including itself!
8NaN === NaN // false
9NaN == NaN // false
10
11// Use Number.isNaN() to check
12Number.isNaN(NaN) // true
13Number.isNaN("hello") // false (doesn't coerce)
14
15// Avoid global isNaN() - it coerces first
16isNaN("hello") // true (coerces to NaN, then checks)
17Number.isNaN("hello") // false (correct)

Infinity and -Infinity

javascript
11 / 0 // Infinity
2-1 / 0 // -Infinity
3Infinity > 1000000 // true
4Infinity === Infinity // true
5
6// Check with isFinite()
7isFinite(100) // true
8isFinite(Infinity) // false
9Number.isFinite(Infinity) // false

The + Operator Precedence

The + operator behaves differently based on operand types:

javascript
1// Left to right evaluation
21 + 2 + "3" // "33" (3 + "3")
3"1" + 2 + 3 // "123" (all string concat)
41 + 2 + 3 + "4" // "64" (6 + "4")
5
6// Unary + converts to number
7+"42" // 42
8+true // 1
9+null // 0
10+undefined // NaN
11+"hello" // NaN
12+[] // 0 ([] -> "" -> 0)
13+{} // NaN

Object to Primitive Conversion

When objects need to become primitives:

javascript
1// Default: valueOf() then toString()
2const obj = {
3 valueOf() { return 42; },
4 toString() { return "hello"; }
5};
6
7obj + 1 // 43 (valueOf used)
8`${obj}` // "hello" (toString for strings)
9
10// Arrays convert via join()
11[1, 2, 3] + "" // "1,2,3"
12[1] + [2] // "12"
13
14// Objects convert via toString()
15{} + [] // 0 (depends on context!)
16[] + {} // "[object Object]"

Short-Circuit Evaluation

Logical operators return actual values, not just booleans:

javascript
1// && returns first falsy or last value
2true && "hello" // "hello"
3false && "hello" // false
4"a" && "b" && "c" // "c"
5"a" && 0 && "c" // 0
6
7// || returns first truthy or last value
8false || "hello" // "hello"
9"a" || "b" // "a"
100 || "" || null // null
11
12// Common patterns
13const name = user.name || "Anonymous";
14const valid = data && data.isValid && data.isValid();

Nullish Coalescing vs OR

Understanding the difference is crucial:

javascript
1// || treats all falsy values the same
20 || "default" // "default" (0 is falsy)
3"" || "default" // "default" ('' is falsy)
4false || "default" // "default"
5
6// ?? only triggers on null/undefined
70 ?? "default" // 0
8"" ?? "default" // ""
9false ?? "default" // false
10null ?? "default" // "default"
11undefined ?? "default" // "default"
12
13// Real-world example
14function getConfig(options) {
15 // BAD: 0 and false would use defaults
16 const timeout = options.timeout || 5000;
17 const enabled = options.enabled || true;
18
19 // GOOD: only null/undefined use defaults
20 const timeout = options.timeout ?? 5000;
21 const enabled = options.enabled ?? true;
22}

Practical Type Checking Patterns

javascript
1// Check for null or undefined
2if (value == null) { /* null or undefined */ }
3if (value === null || value === undefined) { /* same but explicit */ }
4
5// Check for array
6Array.isArray(value)
7
8// Check for plain object
9typeof value === 'object' && value !== null && !Array.isArray(value)
10
11// Check for function
12typeof value === 'function'
13
14// Check for number (excluding NaN)
15typeof value === 'number' && Number.isFinite(value)
16
17// Check for integer
18Number.isInteger(value)
19
20// Check for empty string
21value === ''
22// or truthy string
23typeof value === 'string' && value.length > 0

Summary

  1. Use explicit coercion when you need to convert types
  2. Always use === instead of ==
  3. Understand falsy values - there are only 6
  4. Use ?? for defaults when 0 or '' are valid values
  5. Use Number.isNaN() not isNaN()
  6. Avoid object wrappers like new String()

These concepts will help you avoid subtle bugs and write more predictable JavaScript code.