Skip to main content
JavaScript · lesson 4 of 24
JavaScript beginner Lesson 4 of 24

JavaScript Data Types

Explore JavaScript's seven primitive types and objects, understand typeof, type coercion, and learn how to check types reliably.

By SkillByExample · Updated

JavaScript’s Type System

JavaScript is dynamically typed — variables don’t have fixed types, values do. You can assign a number to a variable and then assign a string to the same variable. This flexibility is powerful but requires understanding how types work to avoid bugs.

JavaScript has 7 primitive types and one complex type: Object. Primitives are immutable and compared by value. Objects are mutable and compared by reference. Knowing the difference between these two categories explains a large class of surprising JavaScript behaviors.

The 7 Primitive Types

1. String

Text data is one of the most common types you’ll work with — user input, labels, messages, API responses. Strings in JavaScript are immutable: every method that looks like it modifies a string actually returns a new one. They can be enclosed in single quotes, double quotes, or backticks.

const single   = 'Hello';
const double   = "World";
const template = `Hello, ${single}!`; // "Hello, Hello!"

// Strings are immutable — methods return new strings
const upper = "javascript".toUpperCase(); // "JAVASCRIPT"
const words = "one two three".split(" "); // ["one", "two", "three"]

console.log(template);
console.log(upper);
console.log(words);
$ node strings.js
Hello, Hello!
JAVASCRIPT
[ 'one', 'two', 'three' ]

Template literals support interpolation and multi-line text — the ${single} is replaced with the variable’s value before the string is created. toUpperCase() returns a new string (the original "javascript" is unchanged) and split() returns a new array of words — neither mutates the original string.

Template literals (backticks) support multi-line strings and expression interpolation — prefer them for anything beyond simple strings.

2. Number

JavaScript has a single number type for both integers and floats, based on the IEEE 754 double-precision standard. This is convenient — you never have to choose between int and float — but it means all numbers share the same precision limits, and floating-point arithmetic has well-known rounding quirks you need to be aware of.

const integer  = 42;
const float    = 3.14;
const negative = -100;
const huge     = 1_000_000; // numeric separator for readability

// Special values
console.log(1 / 0);   // Infinity
console.log(-1 / 0);  // -Infinity
console.log(0 / 0);   // NaN

// Floating point precision — a classic gotcha
console.log(0.1 + 0.2); // 0.30000000000000004  ← not 0.3
console.log(Math.round((0.1 + 0.2) * 10) / 10); // 0.3  ← correct
$ node numbers.js
Infinity
-Infinity
NaN
0.30000000000000004
0.3

1 / 0 produces Infinity — JavaScript doesn’t throw on division by zero, it returns a special IEEE 754 value. 0 / 0 produces NaN (Not a Number) because the result is indeterminate. The 0.1 + 0.2 quirk is the classic floating-point surprise: 0.1 can’t be represented exactly in binary, so the tiny rounding error accumulates. Multiplying by 10, rounding, then dividing restores the expected result.

Safe integer range: Number.MIN_SAFE_INTEGER (-2⁵³+1) to Number.MAX_SAFE_INTEGER (2⁵³-1). Beyond this range, use BigInt.

3. BigInt

Regular Number can’t reliably represent integers beyond 2⁵³-1. This matters for cryptographic operations, high-precision timestamps, and database IDs from systems that use 64-bit integers. BigInt solves this by supporting arbitrary-precision integers, at the cost of not being interchangeable with Number.

const big = 9007199254740991n; // append 'n' to create a BigInt literal
const sum = big + 1n;          // 9007199254740992n

// Cannot mix with Number without explicit conversion
// big + 1; // TypeError: Cannot mix BigInt and other types
BigInt(42) + big; // fine — explicit conversion
$ node bigint.js
9007199254740992n
9007199254741033n

BigInt literals end with n. Arithmetic stays in the BigInt domain: big + 1n yields 9007199254740992n, and BigInt(42) + big yields 9007199254741033n. Mixing BigInt with Number causes a TypeError — you must convert explicitly.

BigInt is used in cryptography, database IDs, and anywhere precise large integer arithmetic matters.

4. Boolean

Booleans represent truth and are the type that powers all conditional logic. Every if statement, every while condition, every && and || expression ultimately works with boolean values — or values that get coerced to them. Knowing which values JavaScript considers “truthy” and which are “falsy” is essential for writing correct conditionals.

const isLoggedIn    = true;
const hasPermission = false;

// Any value can be coerced to boolean
Boolean(0);         // false
Boolean("");        // false
Boolean(null);      // false
Boolean(undefined); // false
Boolean(NaN);       // false
// Everything else is true ("truthy")
Boolean(1);         // true
Boolean("hello");   // true
Boolean([]);        // true  ← empty array is truthy!
Boolean({});        // true  ← empty object is truthy!
$ node booleans.js
false
false
false
false
false
true
true
true
true

The six falsy values — 0, "", null, undefined, NaN, and false — all coerce to false. Everything else is truthy, including empty arrays [] and empty objects {}. This is why checking if (myArray.length > 0) is safer than if (myArray) when you need to know if the array actually contains elements.

The six falsy values — 0, "", null, undefined, NaN, and false — are worth memorizing. Everything else is truthy.

5. null

null is an intentional, explicit absence of value. You assign it when you want to clearly communicate “there is no object here” — for example, when a user is not logged in, or when a search returns no result. Unlike undefined, which the language assigns automatically, null is always set by the programmer on purpose.

let currentUser = null; // not logged in

function findUser(id) {
  const user = db.query(id);
  return user ?? null; // explicitly null if not found
}

6. undefined

undefined is the language’s default “no value.” It’s what a variable holds before you assign it, what a function returns when it has no return statement, and what you get when you access a property that doesn’t exist on an object. You rarely assign undefined yourself — that’s what null is for.

let x;
console.log(x); // undefined — declared but not assigned

function greet(name) {
  console.log(`Hello, ${name}`);
  // no return statement — implicitly returns undefined
}
console.log(greet("Alice")); // logs "Hello, Alice", then undefined

const obj = { a: 1 };
console.log(obj.b); // undefined — property doesn't exist
$ node undefined-demo.js
undefined
Hello, Alice
undefined
undefined

let x; creates the variable but assigns nothing, so it’s undefined. The greet function runs console.log("Hello, Alice") (prints to stdout) and then returns undefined by default — that’s what the outer console.log(greet("Alice")) captures and prints on the next line. Accessing a non-existent property on an object also yields undefined rather than throwing.

7. Symbol

Symbols are unique, immutable primitives used primarily as object property keys. Their key property is guaranteed uniqueness: two Symbols with the same description are never equal. This makes them ideal for adding metadata or extension points to objects without risking name collisions with other code.

const id1 = Symbol("id");
const id2 = Symbol("id");
console.log(id1 === id2); // false — every Symbol is unique

const user = {
  name: "Alice",
  [id1]: 12345 // Symbol as a property key — won't clash with "id" string key
};

// Symbol keys don't appear in for...in or JSON.stringify
console.log(Object.keys(user)); // ["name"]
$ node symbols.js
false
[ 'name' ]

Two Symbols with the same description "id" are never equal — each Symbol() call creates a truly unique value. When used as a computed property key [id1], it stores data on the object without appearing in Object.keys() or JSON.stringify(), making Symbols invisible to most iteration and serialization.

Symbols are used extensively in the JavaScript engine itself (e.g., Symbol.iterator, Symbol.toPrimitive) and in library code to add non-enumerable behavior.

Object

Everything that isn’t a primitive is an Object — including arrays, functions, dates, maps, and sets. Objects are mutable, reference types, meaning two variables can point to the same underlying object. When you pass an object to a function or assign it to another variable, you’re copying the reference, not the object itself.

const person = {
  name: "Alice",
  age: 30,
  greet() {
    return `Hi, I'm ${this.name}`;
  }
};

const colors = ["red", "green", "blue"]; // Array — a special kind of object
console.log(typeof colors);
console.log(Array.isArray(colors));
console.log(person.greet());
$ node objects.js
object
true
Hi, I'm Alice

Arrays are objects (hence typeof returns "object"), but they have array-specific behavior. Array.isArray() is the only reliable check — typeof can’t tell an array from a plain object. Methods like greet() on the person object use this to reference the object’s own properties at call time.

The typeof Operator

typeof is the quickest way to inspect what type a value has at runtime. It returns a string. Most of the time it works exactly as you’d expect, but there are two famous exceptions worth knowing before you hit them in production.

console.log(typeof "hello");
console.log(typeof 42);
console.log(typeof 42n);
console.log(typeof true);
console.log(typeof undefined);
console.log(typeof Symbol());
console.log(typeof {});
console.log(typeof []);           // arrays are objects; use Array.isArray instead
console.log(typeof null);         // famous historical bug; check with === null
console.log(typeof function(){}); // functions are objects, but get their own typeof result
$ node typeof-demo.js
string
number
bigint
boolean
undefined
symbol
object
object
object
function

typeof returns a string naming the type. Two famous gotchas: typeof null returns "object" (a 1995 bug that can’t be fixed without breaking the web), and typeof [] also returns "object" — use Array.isArray() to distinguish arrays from plain objects.

Checking types reliably

Because typeof has those two quirks, certain type checks need a more specific approach:

const value = null;
const arr = [1, 2, 3];

// null check — the only reliable way
console.log(value === null);

// Array check — typeof gives "object", so use this instead
console.log(Array.isArray(arr));

// NaN check — NaN !== NaN, so typeof and == both fail
console.log(Number.isNaN(NaN));
console.log(Number.isNaN("hi")); // unlike the global isNaN which coerces first

// Object check that excludes null
console.log(typeof value === "object" && value !== null);

// Instance check for built-in types
console.log(new Date() instanceof Date);
console.log(new Map() instanceof Map);
$ node type-checks.js
true
true
true
false
false
true
true

For null, the only reliable check is value === null — typeof gives "object", and value == null also catches undefined (which might not be what you want). Array.isArray is the only safe way to test for arrays since typeof returns "object" for them. Number.isNaN is stricter than the global isNaN: it doesn’t coerce its argument first, so "hi" returns false instead of true. The “object check that excludes null” line prints false here because value itself is null — the check is doing its job by rejecting it despite typeof null === "object".

Type Coercion

JavaScript automatically converts types in certain operations. This is called implicit coercion and it’s a source of many surprising behaviors — particularly around the + operator, which doubles as string concatenation, and the loose equality operator ==, which converts types before comparing.

// + with a string converts the other operand to string
console.log("5" + 3);    // number 3 becomes "3"
console.log("5" + true);

// Arithmetic operators (other than +) convert strings to numbers
console.log("5" - 3);
console.log("5" * "2");
console.log("5" ** 2);

// Equality coercion (== coerces, === does not)
console.log(0 == false);         // 0 and false both coerce to 0
console.log(0 === false);        // strict: different types, not equal
console.log("" == false);
console.log(null == undefined);  // the one useful == behavior
console.log(null === undefined);
$ node coercion.js
53
5true
2
10
25
true
false
true
true
false

The + operator coerces any operand to a string when the other is a string — "5" + 3 yields "53", not 8. Arithmetic operators like -, *, ** coerce strings to numbers first. == performs type coercion before comparing (so 0 == false is true), while === does not. The only time == is useful: null == undefined catches both, but === doesn’t.

The safest rule: always use === for equality checks and convert types explicitly when you need to:

const input = "42";

// Explicit conversion — clear intent, no surprises
const num  = Number(input);        // 42
const num2 = parseInt(input, 10);  // 42  — always pass radix 10
const str  = String(42);           // "42"
const bool = Boolean(input);       // true

console.log(num);
console.log(num2);
console.log(str);
console.log(bool);
$ node explicit.js
42
42
42
true

Number("42") parses the full string as a number. parseInt(input, 10) does the same but stops at the first non-digit — parseInt("42abc", 10) would return 42. The radix 10 is important: without it, "010" might be parsed as octal. String(42) converts back to text (console.log prints a bare string without quotes, which is why str shows as 42 and not "42" — the quotes only appear when a string is nested inside a logged object or array). Boolean("42") is true because any non-empty string is truthy.

Checking for null and undefined Together

A common pattern is to check whether a value is either null or undefined — collectively called nullish values. Modern JavaScript has dedicated syntax for this that makes the intent very clear.

function describePort(value) {
  return {
    // Verbose — works but repetitive
    verbose: value === null || value === undefined,
    // Concise — the one acceptable use of == in modern code
    concise: value == null,
    // Modern — nullish coalescing operator
    display: value ?? "default",
  };
}

console.log(describePort(null));
console.log(describePort(undefined));
console.log(describePort(0));  // 0 is NOT nullish — `??` keeps it
console.log(describePort("")); // "" is NOT nullish either
$ node nullish.js
{ verbose: true, concise: true, display: 'default' }
{ verbose: true, concise: true, display: 'default' }
{ verbose: false, concise: false, display: 0 }
{ verbose: false, concise: false, display: '' }

The nullish coalescing operator ?? only falls back on null or undefined — it treats 0, "", and false as valid values, which ?? shows by returning 0 and "" unchanged in the last two calls. This matters when a config value like PORT = 0 is meaningful: PORT || 3000 would give 3000 (wrong — 0 is falsy), but PORT ?? 3000 gives 0 (right — 0 isn’t nullish).

Practice

  1. Type detection: Write a function describeType(value) that returns a string describing the type of value. Handle at least: string, number, bigint, boolean, undefined, symbol, null (special case), array, object, and function. Use typeof and Array.isArray — not instanceof.

  2. BigInt precision: Create a BigInt of value 9007199254740992n (2^53). Add 1n to it and compare the result to the Number 9007199254740993. Use === and also check Number(bigintValue) — explain what you find.

  3. Coercion puzzle: Predict the output of each line, then run to verify:

    • Boolean("false") — is the string "false" truthy or falsy?
    • "5" + 1 + 2 — does JS concatenate or add?
    • "5" - 1 + 2 — what about subtraction?
    • !!null and !!undefined
    • [] + []
    • [] + {}
  4. Truthy/falsy filter: Given [0, 1, "", "hello", null, undefined, NaN, [], {}, false], use .filter(Boolean) to remove all falsy values. What remains?

Solution 1 ```js function describeType(value) { if (value === null) return "null"; if (Array.isArray(value)) return "array"; if (typeof value === "object") return "object"; if (typeof value === "function") return "function"; return typeof value; // "string", "number", "bigint", "boolean", "undefined", "symbol" } ```
Solution 2 ```js const big = 9007199254740992n; const sum = big + 1n; // 9007199254740993n — BigInt arithmetic is exact console.log(sum === BigInt(9007199254740993)); // false! console.log(Number(sum)); // 9007199254740992 — loses precision, same as Number(big) ``` The `false` is the real finding here, not a typo: the literal `9007199254740993` inside `BigInt(9007199254740993)` is parsed as a `Number` *before* `BigInt()` ever sees it, and `Number` can't represent that odd value exactly beyond `2^53` — it silently rounds to `9007199254740992`. So `BigInt(9007199254740993)` actually produces `9007199254740992n`, one less than `sum`. The lesson: once you're past `Number.MAX_SAFE_INTEGER`, only a BigInt *literal* (`9007199254740993n`, with the trailing `n`) preserves the exact value — passing a plain numeric literal through `BigInt(...)` doesn't help, because the precision is already lost by the time it's a `Number`.
Solution 3 ```js console.log(Boolean("false")); // true — non-empty string console.log("5" + 1 + 2); // "512" — string concatenation console.log("5" - 1 + 2); // 6 — subtraction forces numeric coercion console.log(!!null); // false console.log(!!undefined); // false console.log([] + []); // "" — both coerce to empty string console.log([] + {}); // "[object Object]" — {} coerces to string ```
Solution 4 ```js const values = [0, 1, "", "hello", null, undefined, NaN, [], {}, false]; const truthy = values.filter(Boolean); console.log(truthy); // [1, "hello", [], {}, true] ```

What’s Next

The next tutorial covers operators — arithmetic, comparison, logical, and the modern ?? and ?. operators that make null-safe code much cleaner.

Frequently Asked Questions

Why does typeof null return 'object'?
It's a long-standing bug in JavaScript from 1995. In the original implementation, values were tagged with type bits, and null was represented as a null pointer (all zeros) — the same tag used for objects. Fixing it would break millions of websites, so it was never corrected.
What is NaN and why does NaN === NaN return false?
NaN (Not a Number) represents the result of an invalid numeric operation like 0/0 or parseInt('hello'). By IEEE 754 specification, NaN is not equal to anything, including itself. Use Number.isNaN() to check for it reliably.
What's the difference between null and undefined?
undefined means a variable has been declared but not yet assigned a value — it's the language's default 'no value'. null is an explicit assignment meaning 'intentionally empty'. In practice: functions return undefined by default; you set null deliberately to clear a reference.