JavaScript Essentials

Data Types & Type Coercion

45 min Lesson 3 of 60

Understanding Data Types in JavaScript

Every value in JavaScript has a type. Understanding data types is fundamental because they determine what operations you can perform on a value, how values are stored in memory, and how JavaScript behaves when you combine different types. JavaScript is a dynamically typed language, which means variables do not have fixed types -- a variable that holds a number can later hold a string. This flexibility is powerful, but it also means you must understand how types work to avoid subtle bugs.

JavaScript has two categories of data types: primitive types and reference types (objects). In this lesson, we focus entirely on primitive types, which are the building blocks of all data in JavaScript. There are seven primitive types: string, number, bigint, boolean, undefined, null, symbol. Each behaves differently and serves a distinct purpose in your programs.

The String Type

Strings represent textual data. You can create strings using single quotes, double quotes, or backticks (template literals). All three produce string values, but template literals offer additional features like embedded expressions and multi-line strings.

Example: Creating Strings

// Single quotes
let firstName = 'Alice';

// Double quotes
let lastName = "Johnson";

// Template literals (backticks)
let greeting = `Hello, ${firstName} ${lastName}!`;

console.log(greeting); // "Hello, Alice Johnson!"

// Multi-line strings with template literals
let poem = `Roses are red,
Violets are blue,
JavaScript is fun,
And so are you.`;

// String length
console.log(firstName.length); // 5

// Accessing characters
console.log(firstName[0]);       // "A"
console.log(firstName.charAt(2)); // "i"

// Strings are immutable
firstName[0] = 'B'; // This does NOT change the string
console.log(firstName); // Still "Alice"

Strings are immutable in JavaScript. Once a string is created, you cannot change individual characters. Any operation that appears to modify a string actually creates a new string. This is an important concept because it affects performance when you are building large strings through concatenation in a loop -- consider using an array and join() instead.

The Number Type

JavaScript has a single number type that represents both integers and floating-point numbers. Internally, all numbers are stored as 64-bit floating-point values (IEEE 754 double precision). This means JavaScript can represent integers precisely up to 2^53 - 1 (which equals 9,007,199,254,740,991) and floating-point numbers with some inherent precision limitations.

Example: Working with Numbers

// Integer values
let age = 30;
let negative = -15;
let hex = 0xFF;        // 255 in hexadecimal
let octal = 0o77;      // 63 in octal
let binary = 0b1010;   // 10 in binary

// Floating-point values
let price = 19.99;
let scientific = 2.5e6; // 2,500,000

// Special numeric values
let positiveInf = Infinity;
let negativeInf = -Infinity;
let notANumber = NaN;

// Floating-point precision issues
console.log(0.1 + 0.2);         // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false!

// Safe integer range
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991

// Numeric separators for readability (ES2021)
let billion = 1_000_000_000;
let bytes = 0xFF_FF_FF;
console.log(billion); // 1000000000
Common Mistake: The floating-point precision issue (0.1 + 0.2 !== 0.3) is not a JavaScript bug -- it is inherent to how IEEE 754 floating-point numbers work in all programming languages. When comparing floating-point numbers, use a small tolerance value (epsilon): Math.abs(a - b) < Number.EPSILON. For financial calculations, work with integers representing the smallest currency unit (e.g., cents instead of dollars).

Understanding NaN and Infinity

NaN stands for "Not a Number" and represents the result of an undefined or unrepresentable mathematical operation. Despite its name, typeof NaN returns "number". The most surprising property of NaN is that it is not equal to itself -- NaN === NaN returns false. This is the only value in JavaScript with this property.

Example: NaN Behavior

// Operations that produce NaN
console.log(0 / 0);           // NaN
console.log(parseInt('hello')); // NaN
console.log(Math.sqrt(-1));    // NaN
console.log(undefined + 1);    // NaN

// NaN is not equal to itself
console.log(NaN === NaN);  // false
console.log(NaN == NaN);   // false

// Checking for NaN
console.log(isNaN('hello'));       // true (converts first, then checks)
console.log(Number.isNaN('hello')); // false (strict, no conversion)
console.log(Number.isNaN(NaN));     // true

// NaN is contagious -- any arithmetic with NaN produces NaN
console.log(NaN + 5);    // NaN
console.log(NaN * 100);  // NaN
Pro Tip: Always use Number.isNaN() instead of the global isNaN() function. The global isNaN() first attempts to convert its argument to a number, which can produce misleading results. For example, isNaN('hello') returns true because 'hello' converts to NaN, but 'hello' is a string, not NaN. Number.isNaN() only returns true when the argument is actually NaN.

Infinity represents a value greater than any finite number. It results from dividing a positive number by zero or from mathematical overflow. -Infinity is its negative counterpart. You can check for infinity using isFinite() or Number.isFinite().

Example: Infinity

console.log(1 / 0);          // Infinity
console.log(-1 / 0);         // -Infinity
console.log(Infinity + 100); // Infinity
console.log(Infinity - Infinity); // NaN

// Checking for finite values
console.log(Number.isFinite(42));       // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite(NaN));      // false
console.log(Number.isFinite('42'));      // false (no conversion)

The BigInt Type

BigInt was introduced in ES2020 to represent integers of arbitrary precision. Regular numbers lose precision beyond Number.MAX_SAFE_INTEGER, but BigInt can handle integers of any size. You create a BigInt by appending n to an integer literal or by calling BigInt().

Example: BigInt

// Creating BigInt values
let big = 9007199254740993n;
let alsoBig = BigInt("9007199254740993");

// Regular number loses precision
console.log(9007199254740993);  // 9007199254740992 (wrong!)
console.log(9007199254740993n); // 9007199254740993n (correct)

// BigInt arithmetic
console.log(100n + 200n);  // 300n
console.log(100n * 100n);  // 10000n

// Cannot mix BigInt and Number
// console.log(100n + 50); // TypeError!

// Must convert explicitly
console.log(100n + BigInt(50)); // 150n
console.log(Number(100n) + 50); // 150

// BigInt comparison with Number works
console.log(100n === 100); // false (different types)
console.log(100n == 100);  // true (value comparison)

The Boolean Type

Booleans represent logical values: true or false. They are the foundation of all conditional logic in JavaScript. Booleans are commonly produced by comparison operations and logical operators, and they control the flow of your programs through if statements, loops, and ternary expressions.

Example: Boolean Values

let isLoggedIn = true;
let hasPermission = false;

// Comparison operations produce booleans
console.log(5 > 3);    // true
console.log(10 === 10); // true
console.log('a' < 'b');  // true (lexicographic comparison)

// Logical operations with booleans
console.log(true && false);  // false
console.log(true || false);  // true
console.log(!true);          // false

Undefined and Null

undefined and null both represent the absence of a value, but they have different meanings and uses. undefined means a variable has been declared but has not been assigned a value -- it is JavaScript's default "no value" state. null is an intentional assignment that means "no value" or "empty" -- the developer explicitly chose to set it. Think of undefined as "not yet assigned" and null as "intentionally empty."

Example: Undefined vs Null

// undefined -- variable declared but not assigned
let x;
console.log(x);        // undefined
console.log(typeof x);  // "undefined"

// Function with no return value
function doNothing() {}
console.log(doNothing()); // undefined

// Accessing non-existent property
let obj = { name: 'Alice' };
console.log(obj.age);  // undefined

// null -- intentional absence of value
let user = null; // user will be assigned later
console.log(user);        // null
console.log(typeof null);  // "object" (this is a known JavaScript bug!)

// Comparing undefined and null
console.log(undefined == null);  // true (loose equality)
console.log(undefined === null); // false (strict equality)

// Checking for null or undefined
let value = null;
if (value == null) {
    console.log('value is null or undefined');
}
// This is equivalent to:
if (value === null || value === undefined) {
    console.log('value is null or undefined');
}
Note: The fact that typeof null returns "object" instead of "null" is a well-known bug that has existed since the first version of JavaScript. It was never fixed because doing so would break existing code across the web. Always check for null using === null rather than relying on typeof.

The Symbol Type

Symbols are unique identifiers introduced in ES2015 (ES6). Every Symbol is guaranteed to be unique, even if two Symbols have the same description. They are primarily used as property keys to avoid naming conflicts and to create private-like properties on objects.

Example: Symbols

// Creating symbols
let id = Symbol('id');
let anotherId = Symbol('id');

// Every symbol is unique
console.log(id === anotherId); // false

// Using symbols as object keys
let user = {
    name: 'Alice',
    [id]: 12345
};

console.log(user[id]);    // 12345
console.log(user.name);   // "Alice"

// Symbols are not enumerable in for...in loops
for (let key in user) {
    console.log(key); // Only "name" -- symbol key is hidden
}

// Global symbol registry
let globalSym = Symbol.for('app.id');
let sameGlobalSym = Symbol.for('app.id');
console.log(globalSym === sameGlobalSym); // true

The typeof Operator

The typeof operator returns a string indicating the type of a value. It is your primary tool for checking types at runtime. However, it has some quirks you need to be aware of, most notably that typeof null returns "object" and typeof for functions returns "function" even though functions are technically objects.

Example: typeof Results

console.log(typeof "hello");     // "string"
console.log(typeof 42);          // "number"
console.log(typeof 42n);         // "bigint"
console.log(typeof true);        // "boolean"
console.log(typeof undefined);   // "undefined"
console.log(typeof null);        // "object" (bug!)
console.log(typeof Symbol('x')); // "symbol"
console.log(typeof {});          // "object"
console.log(typeof []);          // "object" (arrays are objects)
console.log(typeof function(){}); // "function"

// Practical type checking
function getType(value) {
    if (value === null) return 'null';
    if (Array.isArray(value)) return 'array';
    return typeof value;
}

console.log(getType(null));    // "null"
console.log(getType([1,2,3])); // "array"
console.log(getType(42));      // "number"

Type Coercion: Implicit vs Explicit

Type coercion is the process of converting a value from one type to another. JavaScript performs type coercion in two ways: implicit coercion (automatic, done by the language) and explicit coercion (intentional, done by the developer). Implicit coercion is one of the most confusing aspects of JavaScript and a frequent source of bugs, so understanding it thoroughly is essential.

Implicit Coercion (Automatic)

JavaScript automatically converts types when it encounters an operation between incompatible types. The rules for implicit coercion depend on the operator and the types involved. The most common triggers are the + operator (which can mean addition or string concatenation), comparison operators, and logical contexts (like if statements).

Example: Implicit Coercion with the + Operator

// String concatenation wins when one operand is a string
console.log('5' + 3);     // "53" (number 3 becomes "3")
console.log('5' + true);  // "5true"
console.log('5' + null);  // "5null"
console.log('5' + undefined); // "5undefined"

// Other arithmetic operators convert to numbers
console.log('5' - 3);     // 2
console.log('5' * 3);     // 15
console.log('5' / 2);     // 2.5
console.log('5' % 2);     // 1

// Surprising results
console.log('5' + 3 - 1);   // 52 ("53" - 1 = 52)
console.log(5 + 3 + 'px');   // "8px"
console.log('px' + 5 + 3);   // "px53"

// The unary + converts to number
console.log(+'42');       // 42
console.log(+'');         // 0
console.log(+true);       // 1
console.log(+false);      // 0
console.log(+null);       // 0
console.log(+undefined);  // NaN
Common Mistake: The expression '5' + 3 produces "53" (string), not 8 (number). The + operator performs string concatenation whenever one operand is a string. However, '5' - 3 produces 2 (number) because the - operator only works with numbers and forces conversion. This inconsistency is a major source of bugs. Always convert types explicitly before arithmetic operations.

Explicit Coercion (Intentional)

Explicit coercion is when you deliberately convert a value from one type to another using built-in functions. This is always preferred over relying on implicit coercion because it makes your intent clear and your code more predictable.

Example: Explicit Type Conversions

// Converting to Number
console.log(Number('42'));       // 42
console.log(Number(''));         // 0
console.log(Number('hello'));    // NaN
console.log(Number(true));       // 1
console.log(Number(false));      // 0
console.log(Number(null));       // 0
console.log(Number(undefined));  // NaN

// parseInt and parseFloat
console.log(parseInt('42px'));    // 42 (stops at non-numeric)
console.log(parseInt('0xFF', 16)); // 255
console.log(parseFloat('3.14m')); // 3.14

// Converting to String
console.log(String(42));        // "42"
console.log(String(true));      // "true"
console.log(String(null));      // "null"
console.log(String(undefined)); // "undefined"
console.log(String(NaN));       // "NaN"

// .toString() method
console.log((42).toString());   // "42"
console.log((255).toString(16)); // "ff" (hexadecimal)
console.log((10).toString(2));   // "1010" (binary)

// Converting to Boolean
console.log(Boolean(1));         // true
console.log(Boolean(0));         // false
console.log(Boolean(''));        // false
console.log(Boolean('hello'));   // true
console.log(Boolean(null));      // false
console.log(Boolean(undefined)); // false
console.log(Boolean({}));        // true (objects are always truthy)
console.log(Boolean([]));        // true (even empty arrays!)

Truthy and Falsy Values

In JavaScript, every value is inherently either "truthy" or "falsy." A falsy value is one that becomes false when converted to a boolean. A truthy value becomes true. This matters because JavaScript automatically converts values to booleans in conditional contexts like if statements, ternary operators, and logical operators.

There are exactly eight falsy values in JavaScript. Everything else is truthy:

Example: Falsy Values (Complete List)

// All eight falsy values
console.log(Boolean(false));      // false -- the boolean false itself
console.log(Boolean(0));          // false -- zero
console.log(Boolean(-0));         // false -- negative zero
console.log(Boolean(0n));         // false -- BigInt zero
console.log(Boolean(''));         // false -- empty string
console.log(Boolean(null));       // false -- null
console.log(Boolean(undefined));  // false -- undefined
console.log(Boolean(NaN));        // false -- Not a Number

// Everything else is truthy, including these surprises:
console.log(Boolean('0'));        // true (non-empty string!)
console.log(Boolean('false'));    // true (non-empty string!)
console.log(Boolean([]));         // true (empty array!)
console.log(Boolean({}));         // true (empty object!)
console.log(Boolean(function(){})); // true (function!)
Pro Tip: The double NOT operator (!!) is a common shorthand for converting a value to a boolean. !!value is equivalent to Boolean(value). The first ! converts to boolean and negates, the second ! negates back. For example, !!'hello' returns true and !!0 returns false.

Example: Truthy/Falsy in Conditional Contexts

// Using truthy/falsy in if statements
let username = '';
if (username) {
    console.log(`Welcome, ${username}`);
} else {
    console.log('Please enter your name'); // This runs
}

// Common pattern: default values with ||
let input = '';
let name = input || 'Anonymous';
console.log(name); // "Anonymous"

// Warning: || treats 0 and "" as falsy
let count = 0;
let result = count || 10;
console.log(result); // 10 (not 0!) -- probably not what you want

// Better: use ?? (nullish coalescing) for 0 and ""
let betterResult = count ?? 10;
console.log(betterResult); // 0 -- only null/undefined are replaced

Comparison Pitfalls: == vs ===

JavaScript has two equality operators: loose equality (==) and strict equality (===). The difference is that == performs type coercion before comparing, while === does not -- it requires both the value and the type to match. The type coercion rules of == are complex and counterintuitive, which is why the overwhelming recommendation is to always use ===.

Example: == vs === Comparison

// Strict equality (===) -- no type coercion
console.log(5 === 5);      // true
console.log(5 === '5');    // false (different types)
console.log(0 === false);  // false (different types)
console.log('' === false); // false (different types)
console.log(null === undefined); // false (different types)

// Loose equality (==) -- performs type coercion
console.log(5 == '5');     // true (string "5" becomes number 5)
console.log(0 == false);   // true (false becomes 0)
console.log('' == false);  // true (both become 0)
console.log(null == undefined); // true (special rule)
console.log(null == 0);    // false (null only equals undefined)

// The confusing chain with ==
console.log('' == 0);     // true
console.log(0 == false);  // true
console.log('' == false); // true
// But!
console.log('' == null);  // false (null only == undefined)

// Strange truthy/falsy + equality combos
console.log([] == false);   // true ([] becomes "", "" becomes 0, false becomes 0)
console.log([] == ![]);     // true (! has higher precedence)
console.log('' == []);     // true

// Always use ===
console.log(5 !== '5');    // true (strict inequality)
console.log(null !== undefined); // true
Important: Always use === and !== for comparisons. The only common exception is checking for null or undefined with value == null, which catches both null and undefined in a single check. This is an accepted pattern even in strict codebases because the alternative (value === null || value === undefined) is more verbose with no practical benefit.

Object.is() -- The Strictest Comparison

Object.is() is similar to === but handles two edge cases differently: it considers NaN equal to NaN, and it distinguishes between +0 and -0. While you will rarely need it, it is useful to know it exists for those edge cases.

Example: Object.is()

// Object.is() vs ===
console.log(NaN === NaN);          // false
console.log(Object.is(NaN, NaN));  // true

console.log(+0 === -0);           // true
console.log(Object.is(+0, -0));   // false

// For all other cases, Object.is() behaves like ===
console.log(Object.is(5, 5));     // true
console.log(Object.is(5, '5'));   // false

Type Conversion Summary Table

Here is a comprehensive reference for how different values convert across types. Understanding these conversions will help you predict JavaScript's behavior and write more reliable code:

Reference: Conversion Results

// Value         | Number() | String()      | Boolean()
// ------------- | -------- | ------------- | ---------
// ""            | 0        | ""            | false
// "0"           | 0        | "0"           | true
// "42"          | 42       | "42"          | true
// "hello"       | NaN      | "hello"       | true
// true          | 1        | "true"        | true
// false         | 0        | "false"       | false
// null          | 0        | "null"        | false
// undefined     | NaN      | "undefined"   | false
// 0             | 0        | "0"           | false
// 1             | 1        | "1"           | true
// NaN           | NaN      | "NaN"         | false
// Infinity      | Infinity | "Infinity"    | true
// []            | 0        | ""            | true
// [1]           | 1        | "1"           | true
// {}            | NaN      | "[object Object]" | true
Note: Pay special attention to the difference between Number('') which returns 0 and Number(undefined) which returns NaN. Also note that Number(null) returns 0 while Number(undefined) returns NaN. These inconsistencies are some of the most common traps in JavaScript type coercion.

Practical Patterns for Type Safety

Now that you understand how types work in JavaScript, here are practical patterns you should adopt in your code to avoid type-related bugs and write more robust programs.

Example: Safe Type Checking Patterns

// Safe number parsing
function safeParseNumber(value) {
    if (typeof value === 'number') return value;
    if (typeof value !== 'string') return NaN;
    let trimmed = value.trim();
    if (trimmed === '') return NaN;
    return Number(trimmed);
}

console.log(safeParseNumber('42'));    // 42
console.log(safeParseNumber(' 42 '));  // 42
console.log(safeParseNumber(''));      // NaN (not 0!)
console.log(safeParseNumber(null));    // NaN

// Type-safe comparison function
function isEqual(a, b) {
    // Handle NaN case
    if (Number.isNaN(a) && Number.isNaN(b)) return true;
    return a === b;
}

console.log(isEqual(NaN, NaN)); // true
console.log(isEqual(5, '5'));   // false

// Checking for null or undefined
function isNullish(value) {
    return value == null; // catches both null and undefined
}

console.log(isNullish(null));      // true
console.log(isNullish(undefined)); // true
console.log(isNullish(0));         // false
console.log(isNullish(''));        // false

Practice Exercise

Create a function called describeValue that takes any value and returns a string describing its type and characteristics. The function should: (1) Return "null" for null values instead of "object". (2) Identify arrays as "array" instead of "object". (3) Report whether a number is NaN, Infinity, a safe integer, or a float. (4) Report the length of strings. (5) Report whether a value is truthy or falsy. Test your function with at least 10 different values including edge cases like NaN, 0, empty string, empty array, null, and undefined. Then create a second function called safeAdd that takes two arguments and returns their numeric sum, handling all edge cases: if either argument cannot be converted to a valid number, return 0 instead of NaN. Test it with strings, booleans, null, undefined, and actual numbers.

ES
Edrees Salih
9 hours ago

We are still cooking the magic in the way!