Primitive Data Types

Understanding the building blocks of JavaScript data

JavaScript Data Types Overview

JavaScript has two main categories of data types:

  1. Primitive Types: Basic data types that are immutable (cannot be changed)
  2. Reference Types: Complex data types like objects, arrays, and functions

Today, we'll focus on the primitive data types, which are the fundamental building blocks of JavaScript.

graph TB A[JavaScript Data Types] A --> B[Primitive Types] A --> C[Reference Types] B --> D[String] B --> E[Number] B --> F[Boolean] B --> G[Undefined] B --> H[Null] B --> I[Symbol] B --> J[BigInt] C --> K[Object] K --> L[Array] K --> M[Function] K --> N[Date] K --> O[RegExp] K --> P[Map/Set] K --> Q[Others...]

Analogy: Primitive Types as Atoms

Think of primitive data types as the atoms of programming—they're the simplest, indivisible units that everything else is built from. Just as atoms combine to form molecules with completely different properties (like hydrogen and oxygen forming water), JavaScript primitives combine to create complex data structures with entirely new behaviors and capabilities.

String Type

Strings represent text data in JavaScript. They are sequences of characters enclosed in quotes.

Creating Strings

// Three ways to create a string
const singleQuotes = 'Hello world';
const doubleQuotes = "Hello world";
const backticks = `Hello world`; // Template literals (ES6)

String Properties

const text = "JavaScript";

// Length property
console.log(text.length); // 10

// Accessing characters
console.log(text[0]); // "J"
console.log(text[4]); // "S"

// Strings are immutable - they cannot be changed
text[0] = "j"; // This does nothing
console.log(text); // Still "JavaScript"

Common String Methods

const phrase = "The quick brown fox";

// Converting case
console.log(phrase.toUpperCase()); // "THE QUICK BROWN FOX"
console.log(phrase.toLowerCase()); // "the quick brown fox"

// Finding substrings
console.log(phrase.indexOf("quick")); // 4
console.log(phrase.includes("brown")); // true
console.log(phrase.startsWith("The")); // true
console.log(phrase.endsWith("dog")); // false

// Extracting substrings
console.log(phrase.slice(4, 9)); // "quick"
console.log(phrase.substring(4, 9)); // "quick"
console.log(phrase.split(" ")); // ["The", "quick", "brown", "fox"]

// Trimming whitespace
const paddedText = "   spaced out   ";
console.log(paddedText.trim()); // "spaced out"
console.log(paddedText.trimStart()); // "spaced out   "
console.log(paddedText.trimEnd()); // "   spaced out"

// Replacing content
console.log(phrase.replace("fox", "dog")); // "The quick brown dog"
console.log(phrase.replaceAll("o", "0")); // "The quick br0wn f0x"

Template Literals (ES6+)

const name = "Alice";
const age = 28;

// String concatenation (old way)
const greeting1 = "Hello, " + name + "! You are " + age + " years old.";

// Template literals with variable interpolation
const greeting2 = `Hello, ${name}! You are ${age} years old.`;

console.log(greeting1); // "Hello, Alice! You are 28 years old."
console.log(greeting2); // "Hello, Alice! You are 28 years old."

// Multi-line strings
const multiLine = `This is a string
that spans across
multiple lines.`;

console.log(multiLine);
// This is a string
// that spans across
// multiple lines.

// Expression evaluation in template literals
console.log(`2 + 2 = ${2 + 2}`); // "2 + 2 = 4"
console.log(`${name.toUpperCase()} is ${age < 30 ? 'young' : 'mature'}`); // "ALICE is young"

Real-World Example: Form Validation

Strings are frequently used for form validation in web applications:

function validateEmail(email) {
  // Remove whitespace
  const trimmedEmail = email.trim();
  
  // Check if empty
  if (trimmedEmail.length === 0) {
    return "Email is required";
  }
  
  // Check for @ symbol
  if (!trimmedEmail.includes('@')) {
    return "Email must contain an @ symbol";
  }
  
  // Check for domain
  const parts = trimmedEmail.split('@');
  if (parts.length !== 2 || parts[1].length === 0) {
    return "Email must have a valid domain";
  }
  
  // Check domain has at least one dot
  if (!parts[1].includes('.')) {
    return "Email domain must contain a dot";
  }
  
  // Additional check for domain extension
  const domainParts = parts[1].split('.');
  const extension = domainParts[domainParts.length - 1];
  if (extension.length < 2) {
    return "Email domain must have a valid extension";
  }
  
  return null; // No errors
}

// Test the validation
const emails = [
  "user@example.com",
  "invalid-email",
  "no@domain@test.com",
  "missing.extension@test",
  "   spaces@example.com   "
];

emails.forEach(email => {
  const error = validateEmail(email);
  if (error) {
    console.log(`"${email}" is invalid: ${error}`);
  } else {
    console.log(`"${email}" is valid`);
  }
});

Number Type

The Number type represents both integer and floating-point numbers in JavaScript. Unlike some other programming languages, JavaScript doesn't have separate types for integers and floats.

Creating Numbers

// Integer values
const integer = 42;
const negativeNum = -17;

// Floating point (decimal) values
const float = 3.14159;
const negativeFloat = -2.718;

// Scientific notation
const largeNumber = 1.2e6; // 1.2 * 10^6 = 1,200,000
const tinyNumber = 5e-7;   // 5 * 10^-7 = 0.0000005

// Binary, octal and hexadecimal notations
const binary = 0b1010;  // 10 in decimal
const octal = 0o744;    // 484 in decimal
const hex = 0xFF;       // 255 in decimal

Special Number Values

// Infinity values
const positiveInfinity = Infinity;  // or Number.POSITIVE_INFINITY
const negativeInfinity = -Infinity; // or Number.NEGATIVE_INFINITY

// Not a Number (NaN)
const notANumber = NaN;  // or Number.NaN

// Checking for NaN
console.log(isNaN(NaN));      // true
console.log(isNaN("hello"));  // true (string isn't a number)
console.log(isNaN(42));       // false

// More precise check
console.log(Number.isNaN(NaN));     // true
console.log(Number.isNaN("hello")); // false (specifically checks for NaN value)

Number Range and Precision

// Smallest and largest safe integers
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991

// Smallest and largest values
console.log(Number.MIN_VALUE); // 5e-324 (closest to zero)
console.log(Number.MAX_VALUE); // 1.7976931348623157e+308

// Precision issues with floating point
console.log(0.1 + 0.2);         // 0.30000000000000004, not exactly 0.3
console.log(0.1 + 0.2 === 0.3); // false

// Handling precision issues
console.log(Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON); // true
console.log(Number((0.1 + 0.2).toFixed(1)) === 0.3); // true

Number Methods

const num = 42.3567;

// Conversion methods
console.log(num.toString());      // "42.3567"
console.log(num.toFixed(2));      // "42.36" (string with 2 decimal places)
console.log(num.toPrecision(3));  // "42.4" (string with 3 significant digits)
console.log(num.toExponential()); // "4.23567e+1" (exponential notation)

// Parsing strings to numbers
console.log(parseInt("42"));        // 42
console.log(parseInt("42.95"));     // 42 (truncates decimal part)
console.log(parseInt("42px"));      // 42 (stops at non-numeric character)
console.log(parseInt("0xFF", 16));  // 255 (parses hex with radix 16)

console.log(parseFloat("42.95"));   // 42.95
console.log(parseFloat("42"));      // 42
console.log(parseFloat("42.0px"));  // 42 (stops at non-numeric character)

// Number constructor
console.log(Number("42"));       // 42
console.log(Number("42.5"));     // 42.5
console.log(Number("  42  "));   // 42 (trims whitespace)
console.log(Number("42px"));     // NaN (doesn't parse partial numbers)
console.log(Number(true));       // 1
console.log(Number(false));      // 0
console.log(Number(null));       // 0
console.log(Number(undefined));  // NaN

The Math Object

// Mathematical constants
console.log(Math.PI);    // 3.141592653589793
console.log(Math.E);     // 2.718281828459045

// Rounding methods
console.log(Math.round(4.7));  // 5
console.log(Math.round(4.4));  // 4
console.log(Math.floor(4.9));  // 4 (rounds down)
console.log(Math.ceil(4.1));   // 5 (rounds up)
console.log(Math.trunc(4.9));  // 4 (removes decimal part)

// Min, max and absolute
console.log(Math.min(5, 10, 3, 8)); // 3
console.log(Math.max(5, 10, 3, 8)); // 10
console.log(Math.abs(-42));         // 42

// Power and square root
console.log(Math.pow(2, 3));    // 8 (2^3)
console.log(Math.sqrt(16));     // 4
console.log(Math.cbrt(27));     // 3 (cube root)

// Random numbers
console.log(Math.random());         // Random number between 0 (inclusive) and 1 (exclusive)

// Generate random integer between min and max (inclusive)
function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

console.log(getRandomInt(1, 6)); // Random dice roll (1-6)

Analogy: Floating-Point Precision

JavaScript's floating-point precision issues are like trying to represent 1/3 in decimal form: you can get close (0.333333...), but you can never represent it exactly with a finite number of digits. Similarly, many decimal fractions (like 0.1 and 0.2) can't be represented exactly in binary floating-point format, leading to tiny rounding errors that become visible when you perform calculations.

Real-World Example: Shopping Cart Total

When building an e-commerce application, you need to handle monetary calculations carefully:

function calculateCartTotal(items) {
  // Calculate subtotal
  let subtotal = 0;
  
  items.forEach(item => {
    // Price in dollars, e.g. 10.99
    const itemTotal = item.price * item.quantity;
    subtotal += itemTotal;
  });
  
  // Fix precision issues by converting to cents for calculations
  const subtotalCents = Math.round(subtotal * 100);
  
  // Calculate tax (for example, 8.25%)
  const taxRate = 0.0825;
  const taxCents = Math.round(subtotalCents * taxRate);
  
  // Calculate total
  const totalCents = subtotalCents + taxCents;
  
  // Convert back to dollars for display
  return {
    subtotal: (subtotalCents / 100).toFixed(2),
    tax: (taxCents / 100).toFixed(2),
    total: (totalCents / 100).toFixed(2)
  };
}

// Test with a sample cart
const cart = [
  { name: 'Widget', price: 9.99, quantity: 2 },
  { name: 'Gadget', price: 12.50, quantity: 1 },
  { name: 'Doohickey', price: 5.95, quantity: 3 }
];

console.log(calculateCartTotal(cart));

Boolean Type

The Boolean type has only two possible values: true and false. Booleans are used for conditional logic and represent the concept of truth and falsehood in JavaScript.

Creating Booleans

// Boolean literals
const isActive = true;
const isComplete = false;

// From comparison operations
const isGreater = 5 > 3;           // true
const isEqual = 10 === '10';       // false

// Using the Boolean constructor
const boolTrue = Boolean(1);       // true
const boolFalse = Boolean(0);      // false

Truthy and Falsy Values

In JavaScript, all values have an inherent Boolean value when used in a Boolean context, known as "truthy" or "falsy."

Falsy values (convert to false):

// All of these are falsy:
console.log(Boolean(false));      // false
console.log(Boolean(0));          // false
console.log(Boolean(-0));         // false
console.log(Boolean(0n));         // false (BigInt zero)
console.log(Boolean(""));         // false (empty string)
console.log(Boolean(null));       // false
console.log(Boolean(undefined));  // false
console.log(Boolean(NaN));        // false

Truthy values (convert to true):

// All other values are truthy, including:
console.log(Boolean(true));       // true
console.log(Boolean(42));         // true (any non-zero number)
console.log(Boolean("false"));    // true (non-empty string, even "false")
console.log(Boolean({}));         // true (empty object)
console.log(Boolean([]));         // true (empty array)
console.log(Boolean(function(){})); // true (functions)

Logical Operators

// Logical AND (&&) - returns first falsy value or last truthy value
console.log(true && false);             // false
console.log(false && anyExpression);    // false (short-circuits, doesnt evaluate second expression)
console.log("Hello" && 42 && true);     // true (last value)
console.log("Hello" && 0 && true);      // 0 (first falsy value)

// Logical OR (||) - returns first truthy value or last falsy value
console.log(true || false);             // true
console.log(true || anyExpression);     // true (short-circuits)
console.log(false || "" || null);       // null (last value)
console.log(false || 0 || "Hello");     // "Hello" (first truthy value)

// Logical NOT (!) - inverts truthiness
console.log(!true);                     // false
console.log(!false);                    // true
console.log(!"Hello");                  // false (string is truthy)
console.log(!"");                       // true (empty string is falsy)

// Double NOT (!!) - converts to Boolean
console.log(!!"Hello");                 // true
console.log(!!0);                       // false

Nullish Coalescing Operator (??)

ES2020 introduced the nullish coalescing operator, which is similar to OR but only treats null and undefined as falsy.

// The ?? operator only considers null and undefined as falsy
const value1 = 0 ?? "default";     // 0 (0 is a valid value)
const value2 = "" ?? "default";    // "" (empty string is a valid value)
const value3 = null ?? "default";  // "default"
const value4 = undefined ?? "default"; // "default"

// Compare with OR
const withOr1 = 0 || "default";    // "default" (0 is falsy)
const withOr2 = "" || "default";   // "default" (empty string is falsy)

Analogy: Truthy and Falsy as Light Switches

Think of JavaScript's truthiness as light switches. Some values are like switches that are definitely "off" (falsy values like false, 0, "", null, undefined, and NaN), and all other values are like switches that are definitely "on" (truthy). When JavaScript needs to make a decision based on a condition, it checks whether the switch is on or off, regardless of what type of value it is.

Real-World Example: Form Validation with Conditional Logic

Boolean logic is essential for form validation:

function validateUserInput(username, email, password) {
  const errors = {};
  
  // Username validation
  if (!username) {
    errors.username = "Username is required";
  } else if (username.length < 3) {
    errors.username = "Username must be at least 3 characters";
  }
  
  // Email validation
  if (!email) {
    errors.email = "Email is required";
  } else if (!email.includes('@') || !email.includes('.')) {
    errors.email = "Email format is invalid";
  }
  
  // Password validation
  if (!password) {
    errors.password = "Password is required";
  } else {
    const hasMinLength = password.length >= 8;
    const hasUppercase = /[A-Z]/.test(password);
    const hasLowercase = /[a-z]/.test(password);
    const hasNumber = /[0-9]/.test(password);
    
    if (!(hasMinLength && hasUppercase && hasLowercase && hasNumber)) {
      errors.password = "Password must be at least 8 characters with uppercase, lowercase, and numbers";
    }
  }
  
  // Return validation result
  return {
    isValid: Object.keys(errors).length === 0,
    errors: errors
  };
}

// Test the validation
const result = validateUserInput("joe", "invalid-email", "password123");
console.log(result.isValid);  // false
console.log(result.errors);   // Object with validation errors

Undefined Type

The undefined type has only one value: undefined. It represents a variable that has been declared but not assigned a value.

When Undefined Occurs

// Variable declared but not initialized
let uninitializedVar;
console.log(uninitializedVar); // undefined

// Missing function parameters
function greet(name) {
  console.log(`Hello, ${name}`);
}
greet(); // "Hello, undefined"

// Missing object properties
const user = { name: "Alice" };
console.log(user.age); // undefined

// Function with no return statement
function noReturn() {
  // No return statement
}
console.log(noReturn()); // undefined

Checking for Undefined

// Using strict equality
function processValue(value) {
  if (value === undefined) {
    return "No value provided";
  }
  return `Processing: ${value}`;
}

// Using typeof
function safelyProcessValue(value) {
  if (typeof value === "undefined") {
    return "No value provided";
  }
  return `Processing: ${value}`;
}

// Avoid this pattern (global undefined can be overwritten in older browsers)
function badCheck(value) {
  if (value == undefined) { // uses loose equality, also matches null
    return "No value provided";
  }
  return `Processing: ${value}`;
}

Optional Chaining (?.) for Undefined

ES2020 introduced optional chaining to safely access nested properties that might be undefined.

// Object with nested properties
const user = {
  name: "Alice",
  address: {
    city: "Wonderland"
  }
};

// Old way (error-prone)
let zipCode;
if (user && user.address && user.address.zipCode) {
  zipCode = user.address.zipCode;
} else {
  zipCode = "Unknown";
}

// With optional chaining
const zipCode2 = user?.address?.zipCode ?? "Unknown";

// Also works with function calls
const result = user.getDetails?.() ?? "Method not available";

Analogy: Undefined as an Empty Slot

Think of undefined as an empty slot in a mailbox. The slot exists (the variable has been declared), but there's no mail in it yet (no value has been assigned). It's different from not having a slot at all (undeclared variable), and it's also different from having a slot with an explicit "no mail" sign (which would be null).

Real-World Example: Default Function Parameters

Handling undefined is common when working with function parameters:

// Old way: Check for undefined and provide defaults
function createUser(username, role, isActive) {
  // Check each parameter and provide default if undefined
  username = typeof username !== 'undefined' ? username : 'guest';
  role = typeof role !== 'undefined' ? role : 'user';
  isActive = typeof isActive !== 'undefined' ? isActive : true;
  
  return {
    username,
    role,
    isActive,
    createdAt: new Date()
  };
}

// ES6+ way: Default parameters
function createUser(username = 'guest', role = 'user', isActive = true) {
  return {
    username,
    role,
    isActive,
    createdAt: new Date()
  };
}

console.log(createUser('alice'));
// { username: 'alice', role: 'user', isActive: true, createdAt: Date }

console.log(createUser(undefined, 'admin'));
// { username: 'guest', role: 'admin', isActive: true, createdAt: Date }

// Note: null will not trigger default parameters
console.log(createUser(null, 'admin'));
// { username: null, role: 'admin', isActive: true, createdAt: Date }

Null Type

The null type has only one value: null. It represents the intentional absence of any object value or intentional non-value.

Using Null

// Explicitly assigning null
let user = null; // User data not yet loaded

// Returning null from a function to indicate "no valid result"
function findUser(id) {
  // No user found
  return null;
}

// Checking for null
const result = findUser(42);
if (result === null) {
  console.log("User not found");
}

Null vs Undefined

Aspect null undefined
Meaning Intentional absence of value Variable declared but not assigned
Assignment Must be explicitly assigned Automatically assigned
Type typeof null is "object" (historical bug) typeof undefined is "undefined"
JSON Valid JSON value Not valid in JSON
Equality null == undefined is true (loose equality)
Strict Equality null === undefined is false (strict equality)

Checking for Null or Undefined

// Checking for null specifically
if (value === null) {
  console.log("Value is null");
}

// Checking for either null or undefined (not recommended)
if (value == null) {
  console.log("Value is null or undefined");
}

// Better approach - check each explicitly
if (value === null || value === undefined) {
  console.log("Value is null or undefined");
}

// Using nullish coalescing operator
const result = value ?? defaultValue; // result is defaultValue if value is null or undefined

Analogy: null vs undefined

Think of undefined and null like different types of empty:

  • undefined is like an empty space on a form that hasn't been filled out yet—nobody has written anything there.
  • null is like writing "N/A" on a form—you've explicitly indicated that there is no applicable value for this field.

Real-World Example: API Data Handling

When working with APIs, null is often used to represent missing data:

// Function to fetch user data from an API
async function fetchUserProfile(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    
    if (!response.ok) {
      if (response.status === 404) {
        // User not found
        return null;
      }
      throw new Error(`API error: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    console.error("Failed to fetch user:", error);
    return null;
  }
}

// Using the function
async function displayUserProfile(userId) {
  const profile = await fetchUserProfile(userId);
  
  if (profile === null) {
    // Handle the "no user" case
    displayErrorMessage("User not found or could not be loaded");
    return;
  }
  
  // Handle the valid user case
  displayUserData(profile);
}

// Optional chaining makes dealing with potentially null data easier
function renderUserLocation(user) {
  const locationText = user?.address?.city ?? "Location unknown";
  return locationText;
}

Symbol Type

Symbols, introduced in ES6, are a primitive type for unique identifiers. Each Symbol value is unique and immutable, making them useful for object property keys that shouldn't conflict with other properties.

Creating Symbols

// Creating a Symbol
const sym1 = Symbol();
const sym2 = Symbol();

console.log(sym1 === sym2); // false, all Symbols are unique

// Symbols with descriptions (for debugging)
const userIdSymbol = Symbol('userId');
console.log(userIdSymbol.toString()); // "Symbol(userId)"
console.log(userIdSymbol.description); // "userId"

// Symbol.for() creates global Symbols
const globalSym1 = Symbol.for('globalId');
const globalSym2 = Symbol.for('globalId');

console.log(globalSym1 === globalSym2); // true, same Symbol from registry

// Get Symbol key from global registry
console.log(Symbol.keyFor(globalSym1)); // "globalId"
console.log(Symbol.keyFor(sym1)); // undefined (not in global registry)

Using Symbols as Object Keys

// Using Symbols as object property keys
const id = Symbol('id');
const user = {
  name: "Alice",
  [id]: 42 // Symbol as property key
};

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

// Symbol properties are not enumerable in typical iterations
console.log(Object.keys(user)); // ["name"]
console.log(Object.getOwnPropertyNames(user)); // ["name"]

// To get Symbol properties
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]

// To get all properties, including Symbols
console.log(Reflect.ownKeys(user)); // ["name", Symbol(id)]

Built-in Symbols

JavaScript has several built-in "well-known" Symbols that allow you to customize object behavior.

// Custom iteration behavior using Symbol.iterator
const customIterable = {
  items: ['a', 'b', 'c'],
  
  // Define custom iterator
  [Symbol.iterator]: function* () {
    for (let item of this.items) {
      yield item.toUpperCase();
    }
  }
};

// Now we can iterate
for (const item of customIterable) {
  console.log(item); // "A", "B", "C"
}

// Custom object conversion using Symbol.toPrimitive
const customObject = {
  name: "Special Object",
  value: 42,
  
  // Define conversion behavior
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return this.value;
      case 'string':
        return this.name;
      default: // 'default' hint
        return `${this.name} (${this.value})`;
    }
  }
};

console.log(+customObject); // 42 (number conversion)
console.log(`${customObject}`); // "Special Object" (string conversion)
console.log(customObject + ''); // "Special Object (42)" (default conversion)

Analogy: Symbols as VIP Access Badges

Think of Symbols as VIP access badges at an exclusive event. Each badge is unique and can't be duplicated or forged. If you have the badge, you can access special features (object properties) that aren't available to the general public. Also, these badges don't show up in the public guest list (regular enumeration of object properties), giving you a way to add "hidden" properties to objects.

Real-World Example: Creating a Singleton Pattern

Symbols can be used to implement the Singleton pattern in JavaScript:

// Using Symbol for a private instance holder
const instance = Symbol('instance');

class Database {
  constructor(connectionString) {
    if (Database[instance]) {
      return Database[instance];
    }
    
    this.connectionString = connectionString;
    this.isConnected = false;
    console.log("Creating new database connection");
    
    // Store the instance
    Database[instance] = this;
  }
  
  connect() {
    if (this.isConnected) {
      console.log("Already connected");
      return;
    }
    
    console.log(`Connecting to ${this.connectionString}`);
    this.isConnected = true;
  }
  
  query(sql) {
    if (!this.isConnected) {
      this.connect();
    }
    
    console.log(`Executing query: ${sql}`);
    return [`Result for ${sql}`];
  }
}

// Usage
const db1 = new Database("mongodb://example.com/mydb");
db1.connect();

const db2 = new Database("mysql://example.com/db"); // No new connection
console.log(db1 === db2); // true - same instance

db2.query("SELECT * FROM users"); // Uses the existing connection

BigInt Type

The BigInt type, introduced in ES2020, represents integers with arbitrary precision, overcoming the limitations of the Number type which can only safely represent integers between -(2^53-1) and 2^53-1.

Creating BigInts

// Using the n suffix
const bigInt1 = 1234567890123456789012345678901234567890n;

// Using the BigInt() function
const bigInt2 = BigInt("9007199254740991");
const bigInt3 = BigInt(9007199254740991); // From Number

// Maximum safe integer in Number type
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991

// BigInt can represent integers beyond this limit
const beyondSafeInteger = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
console.log(beyondSafeInteger); // 9007199254740992n

// Compare with regular Number behavior
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2); // true (precision loss)
console.log(BigInt(Number.MAX_SAFE_INTEGER) + 1n === BigInt(Number.MAX_SAFE_INTEGER) + 2n); // false (correct)

Operations with BigInt

// Arithmetic operations
console.log(5n + 3n); // 8n
console.log(5n - 3n); // 2n
console.log(5n * 3n); // 15n
console.log(5n / 3n); // 1n (truncates, no floating point)
console.log(5n % 3n); // 2n
console.log(5n ** 3n); // 125n

// Cannot mix BigInt and Number
// console.log(5n + 3); // TypeError
// Workaround: Convert explicitly
console.log(5n + BigInt(3)); // 8n
console.log(Number(5n) + 3); // 8

// Comparison operators work
console.log(5n > 3); // true
console.log(5n === 5); // false (different types)
console.log(5n == 5); // true (loose equality)

// Bitwise operations also work
console.log(8n >> 1n); // 4n
console.log(8n << 1n); // 16n
console.log(8n & 7n); // 0n
console.log(8n | 7n); // 15n

Limitations of BigInt

// Cannot use with Math object
// console.log(Math.max(1n, 2n)); // TypeError

// No decimal point - not for floating point operations
// console.log(5n / 2n); // 2n, not 2.5n

// JSON serialization doesn't work directly
const obj = { value: 123456789123456789n };
// console.log(JSON.stringify(obj)); // TypeError

// Workaround: convert to string
const jsonSafe = { value: obj.value.toString() };
console.log(JSON.stringify(jsonSafe)); // {"value":"123456789123456789"}

Analogy: BigInt as Expandable Storage

Think of regular Numbers as a fixed-size storage box that can only fit values up to a certain size. Once you exceed that size, items get misplaced or damaged (precision loss). BigInts are like expandable storage boxes that can grow to accommodate numbers of any size, no matter how large they get—but they can only store whole items (integers), not partial ones (decimals).

Real-World Example: Cryptography and Financial Calculations

BigInt is particularly useful for cryptography, where large integers are common:

// Basic modular exponentiation (common in crypto)
function modPow(base, exponent, modulus) {
  // Convert to BigInt to handle large numbers
  base = BigInt(base);
  exponent = BigInt(exponent);
  modulus = BigInt(modulus);
  
  let result = 1n;
  base = base % modulus;
  
  while (exponent > 0n) {
    if (exponent % 2n === 1n) {
      result = (result * base) % modulus;
    }
    exponent = exponent >> 1n;
    base = (base * base) % modulus;
  }
  
  return result;
}

// RSA-like operation (simplified)
// Public key components
const e = 65537n;
const n = 999331999191171934862582056475721892679569n;

// Message to encrypt (convert text to number for demo)
const message = 123456789n;

// Encrypt: (message^e) % n
const encrypted = modPow(message, e, n);
console.log("Encrypted:", encrypted);

// Financial calculations with precise integers
function calculateCompoundInterest(principal, ratePercent, years, compoundingsPerYear) {
  // Convert to BigInt and use a scaling factor for decimal calculations
  const SCALE = 1000000n; // 6 decimal places of precision
  
  const p = BigInt(Math.floor(principal * 1000000)) * SCALE / 1000000n;
  const r = BigInt(Math.floor(ratePercent * 1000000)) * SCALE / 1000000n;
  const t = BigInt(Math.floor(years * 1000000)) * SCALE / 1000000n;
  const n = BigInt(Math.floor(compoundingsPerYear * 1000000)) * SCALE / 1000000n;
  
  // Calculate (1 + r/n)^(n*t)
  const base = SCALE * n + r * SCALE / (n * 100n); // (1 + r/n) scaled
  const exponent = n * t / SCALE;
  
  // Integer power calculation
  let result = SCALE;
  let expBase = base;
  let exp = exponent;
  
  while (exp > 0n) {
    if (exp % 2n === 1n) {
      result = (result * expBase) / SCALE;
    }
    exp = exp / 2n;
    expBase = (expBase * expBase) / SCALE;
  }
  
  // Final amount = principal * result
  const amount = (p * result) / (SCALE * SCALE);
  
  return Number(amount) / 1000000;
}

Type Checking and Conversion

Being able to check and convert between primitive types is essential for robust JavaScript programming.

The typeof Operator

// Using typeof to check types
console.log(typeof "Hello");               // "string"
console.log(typeof 42);                    // "number"
console.log(typeof true);                  // "boolean"
console.log(typeof undefined);             // "undefined"
console.log(typeof Symbol());              // "symbol"
console.log(typeof 42n);                   // "bigint"

// Quirks of typeof
console.log(typeof null);                  // "object" (historical bug)
console.log(typeof []);                    // "object" (arrays are objects)
console.log(typeof function() {});         // "function"
console.log(typeof NaN);                   // "number" (NaN is a number)

Type Conversion

// String conversions
console.log(String(42));                  // "42"
console.log(String(true));                // "true"
console.log(String(null));                // "null"
console.log(String(undefined));           // "undefined"
console.log(String(Symbol('sym')));       // "Symbol(sym)"
console.log(String(123n));                // "123"

// Number conversions
console.log(Number("42"));                // 42
console.log(Number("42.5"));              // 42.5
console.log(Number(""));                  // 0
console.log(Number(true));                // 1
console.log(Number(false));               // 0
console.log(Number(null));                // 0
console.log(Number(undefined));           // NaN
console.log(Number("hello"));             // NaN
console.log(Number(42n));                 // 42

// Boolean conversions
console.log(Boolean(42));                 // true
console.log(Boolean(0));                  // false
console.log(Boolean("hello"));            // true
console.log(Boolean(""));                 // false
console.log(Boolean(null));               // false
console.log(Boolean(undefined));          // false
console.log(Boolean(NaN));                // false

// BigInt conversions
console.log(BigInt(42));                  // 42n
console.log(BigInt("42"));                // 42n
console.log(BigInt(true));                // 1n
// console.log(BigInt(1.5));              // RangeError
// console.log(BigInt("1.5"));            // SyntaxError

Implicit Type Conversion (Coercion)

// String coercion with +
console.log("2" + 3);                    // "23" (number converted to string)
console.log(2 + "3");                    // "23" (number converted to string)

// Numeric coercion with comparison
console.log("2" > 1);                    // true (string converted to number)
console.log("2" < "12");                 // false (string comparison, not numeric)

// Boolean coercion in logical operations
console.log("hello" && 42);              // 42 (both truthy)
console.log(0 || "hello");               // "hello" (0 is falsy)
console.log(null || undefined);          // undefined (both falsy)

// Numeric coercion with mathematical operators
console.log("5" - 2);                    // 3 (string converted to number)
console.log("5" * 2);                    // 10 (string converted to number)
console.log("5" / 2);                    // 2.5 (string converted to number)
graph TD A[Type Conversion] --> B[Explicit Conversion] A --> C[Implicit Conversion] B --> D[String()] B --> E[Number()] B --> F[Boolean()] B --> G[BigInt()] C --> H[+ operator with string] C --> I[Mathematical operators] C --> J[Comparison operators] C --> K[Logical operators] C --> L[Control structures if, while, etc.]

Real-World Example: Processing User Input

Type checking and conversion is essential when dealing with user input:

// Form processing function
function processUserSettings(formData) {
  // Validate and normalize settings
  const settings = {
    // Username - ensure string, trim, default if empty
    username: typeof formData.username === 'string' 
      ? formData.username.trim() || 'Anonymous'
      : 'Anonymous',
      
    // Age - convert to number, ensure valid
    age: Number(formData.age),
    
    // Theme - ensure valid option
    theme: ['light', 'dark', 'system'].includes(formData.theme)
      ? formData.theme
      : 'system',
      
    // Notifications - convert to boolean
    notifications: Boolean(formData.notifications),
    
    // Max file size - ensure valid number or use default
    maxFileSize: typeof formData.maxFileSize !== 'undefined'
      ? Number(formData.maxFileSize) || 5
      : 5,
      
    // Processed timestamp
    processedAt: new Date().toISOString()
  };
  
  // Validate age specifically
  if (isNaN(settings.age) || settings.age < 13) {
    throw new Error('Age must be a number and at least 13');
  }
  
  return settings;
}

// Test with user input
const userInput = {
  username: '  Alice   ',
  age: '28',
  theme: 'dark',
  notifications: 1,
  maxFileSize: '10'
};

try {
  const settings = processUserSettings(userInput);
  console.log(settings);
} catch (error) {
  console.error('Invalid settings:', error.message);
}

Primitive vs Reference Types

Understanding the difference between primitive and reference types is crucial for avoiding bugs in JavaScript.

Value Comparison

// Primitive types are compared by value
const num1 = 5;
const num2 = 5;
console.log(num1 === num2); // true

const str1 = "hello";
const str2 = "hello";
console.log(str1 === str2); // true

// Reference types are compared by reference (memory address)
const obj1 = { name: "Alice" };
const obj2 = { name: "Alice" };
console.log(obj1 === obj2); // false (different objects)

const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
console.log(arr1 === arr2); // false (different arrays)

// Same reference
const obj3 = obj1;
console.log(obj1 === obj3); // true (same object reference)

Immutability of Primitives

// Primitives are immutable
let str = "hello";
str[0] = "H"; // No effect
console.log(str); // still "hello"

let num = 42;
num.newProp = "test"; // No effect, no error in non-strict mode
console.log(num.newProp); // undefined

// When we "modify" primitives, we're actually creating new values
let message = "hello";
message = message + " world"; // Creates a new string
console.log(message); // "hello world"

// References can be modified
const user = { name: "Alice" };
user.name = "Bob"; // Modifies the object
console.log(user.name); // "Bob"

Passing to Functions

// Primitive types are passed by value
function modifyPrimitive(value) {
  value = 100; // Modifies the local copy only
}

let num = 42;
modifyPrimitive(num);
console.log(num); // Still 42

// Reference types are passed by reference
function modifyObject(obj) {
  obj.modified = true; // Modifies the original
}

const data = { name: "Original" };
modifyObject(data);
console.log(data.modified); // true

// Reassigning the parameter doesn't affect the original reference
function replaceObject(obj) {
  obj = { name: "Replacement" }; // Local reassignment only
}

const original = { name: "Original" };
replaceObject(original);
console.log(original.name); // Still "Original"

Analogy: Primitives vs References

Think of primitive values like numbers on a piece of paper: when you pass the number to someone, you're giving them a copy of the number. If they change their copy, your original remains unchanged.

Reference types are more like a remote control: when you pass an object to a function, you're passing the same remote control, not a copy. If the receiver presses buttons (modifies properties), those changes affect the actual device (the original object).

Real-World Example: User Profile Update

// A profile update function
function updateUserProfile(profile, updates) {
  // Common mistake: assuming primitive properties are automatically updated
  if (updates.name) {
    profile.name = updates.name;
  }
  
  // References get merged, not copied
  if (updates.preferences) {
    // WRONG approach (reference gets overwritten)
    // profile.preferences = updates.preferences;
    
    // CORRECT approach (merge properties)
    Object.assign(profile.preferences, updates.preferences);
  }
  
  // Arrays need special handling
  if (updates.tags) {
    // WRONG approach (direct reference assignment)
    // profile.tags = updates.tags;
    
    // CORRECT approach (create a new array)
    profile.tags = [...updates.tags];
  }
  
  // Nested objects need deep cloning
  if (updates.address) {
    // Deep clone
    profile.address = JSON.parse(JSON.stringify(updates.address));
  }
  
  // Add update timestamp
  profile.lastUpdated = new Date();
  
  return profile;
}

// Test the function
const userProfile = {
  id: 42,
  name: "Alice",
  preferences: {
    theme: "light",
    notifications: true
  },
  tags: ["developer", "javascript"],
  address: {
    city: "San Francisco",
    country: "USA"
  }
};

const updates = {
  name: "Alice Smith",
  preferences: {
    theme: "dark"
  },
  tags: ["developer", "javascript", "react"],
  address: {
    city: "New York",
    country: "USA"
  }
};

updateUserProfile(userProfile, updates);
console.log(userProfile);
// The profile now has updated values while preserving other properties

Practical Exercise

Let's practice working with primitive types through exercises of increasing complexity.

Exercise 1: Type Identification and Conversion

Create a function that identifies the type of a value and converts it to a specified target type.

function convertValue(value, targetType) {
  // Get the current type
  let currentType = typeof value;
  
  // Special case for null
  if (value === null) {
    currentType = "null";
  }
  
  // Handle conversion
  switch (targetType) {
    case "string":
      return String(value);
    case "number":
      return Number(value);
    case "boolean":
      return Boolean(value);
    case "bigint":
      try {
        // BigInt only works with integers
        if (Number.isInteger(Number(value))) {
          return BigInt(value);
        }
        throw new Error("Value cannot be converted to BigInt");
      } catch (e) {
        throw new Error(`Cannot convert ${value} to BigInt: ${e.message}`);
      }
    default:
      throw new Error(`Unsupported target type: ${targetType}`);
  }
}

// Test the function with various inputs
console.log(convertValue(42, "string"));      // "42"
console.log(convertValue("42", "number"));    // 42
console.log(convertValue(0, "boolean"));      // false
console.log(convertValue("true", "boolean")); // true
console.log(convertValue(123, "bigint"));     // 123n
try {
  console.log(convertValue(1.5, "bigint"));   // Error
} catch (e) {
  console.error(e.message);
}

Exercise 2: String Manipulation Challenge

Create a set of string utility functions for common operations:

const StringUtils = {
  // Capitalize the first letter of each word
  capitalize(str) {
    if (typeof str !== 'string') return '';
    
    return str
      .split(' ')
      .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
      .join(' ');
  },
  
  // Truncate string with ellipsis if too long
  truncate(str, maxLength = 20) {
    if (typeof str !== 'string') return '';
    if (str.length <= maxLength) return str;
    
    return str.slice(0, maxLength - 3) + '...';
  },
  
  // Count occurrences of a substring
  countOccurrences(str, substr) {
    if (typeof str !== 'string' || typeof substr !== 'string') return 0;
    if (substr.length === 0) return 0;
    
    let count = 0;
    let position = str.indexOf(substr);
    
    while (position !== -1) {
      count++;
      position = str.indexOf(substr, position + 1);
    }
    
    return count;
  },
  
  // Convert camelCase to snake_case
  camelToSnake(str) {
    if (typeof str !== 'string') return '';
    
    return str.replace(/([A-Z])/g, '_$1').toLowerCase();
  },
  
  // Convert snake_case to camelCase
  snakeToCamel(str) {
    if (typeof str !== 'string') return '';
    
    return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
  }
};

// Test the string utilities
console.log(StringUtils.capitalize("hello world"));  // "Hello World"
console.log(StringUtils.truncate("This is a very long sentence that needs truncating", 20));  // "This is a very lon..."
console.log(StringUtils.countOccurrences("hello hello world", "hello"));  // 2
console.log(StringUtils.camelToSnake("thisIsCamelCase"));  // "this_is_camel_case"
console.log(StringUtils.snakeToCamel("this_is_snake_case"));  // "thisIsSnakeCase"

Exercise 3: Advanced Number Formatting

Create a comprehensive number formatting utility that handles different formats and scales:

const NumberFormatter = {
  // Format as currency
  currency(value, currency = 'USD', locale = 'en-US') {
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency: currency
    }).format(value);
  },
  
  // Format with specified decimal places
  decimal(value, decimalPlaces = 2, locale = 'en-US') {
    return new Intl.NumberFormat(locale, {
      minimumFractionDigits: decimalPlaces,
      maximumFractionDigits: decimalPlaces
    }).format(value);
  },
  
  // Format as percentage
  percent(value, decimalPlaces = 0, locale = 'en-US') {
    return new Intl.NumberFormat(locale, {
      style: 'percent',
      minimumFractionDigits: decimalPlaces,
      maximumFractionDigits: decimalPlaces
    }).format(value);
  },
  
  // Format with unit
  unit(value, unit, locale = 'en-US') {
    return new Intl.NumberFormat(locale, {
      style: 'unit',
      unit: unit
    }).format(value);
  },
  
  // Format large numbers with K, M, B suffixes
  compact(value, locale = 'en-US') {
    return new Intl.NumberFormat(locale, {
      notation: 'compact',
      compactDisplay: 'short'
    }).format(value);
  },
  
  // Format as file size (B, KB, MB, GB)
  fileSize(bytes) {
    const units = ['B', 'KB', 'MB', 'GB', 'TB'];
    let size = bytes;
    let unitIndex = 0;
    
    while (size >= 1024 && unitIndex < units.length - 1) {
      size /= 1024;
      unitIndex++;
    }
    
    return `${size.toFixed(size < 10 && unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
  }
};

// Test the number formatter
console.log(NumberFormatter.currency(1234.56));         // "$1,234.56"
console.log(NumberFormatter.currency(1234.56, 'EUR'));  // "€1,234.56"
console.log(NumberFormatter.decimal(1234.5678, 3));     // "1,234.568"
console.log(NumberFormatter.percent(0.1234));           // "12%"
console.log(NumberFormatter.percent(0.1234, 1));        // "12.3%"
console.log(NumberFormatter.unit(25, 'celsius'));       // "25°C"
console.log(NumberFormatter.compact(1234567));          // "1.2M"
console.log(NumberFormatter.fileSize(1536));            // "1.5 KB"
console.log(NumberFormatter.fileSize(1073741824));      // "1 GB"

Key Takeaways

Additional Resources