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.

Deep Dive: Type Coercion & Edge Cases - Anko Academy