Syntax and Types
JavaScript has exactly eight types: seven primitives and object. Everything you will ever hold in a variable is one of them. This page covers what each one is, how to declare variables, and the parsing rules that occasionally bite.
The eight types
Section titled “The eight types”| Type | Example values | typeof result |
|---|---|---|
number |
42, -0.5, NaN, Infinity |
'number' |
bigint |
9007199254740993n |
'bigint' |
string |
'hi', `x=${x}` |
'string' |
boolean |
true, false |
'boolean' |
undefined |
undefined |
'undefined' |
null |
null |
'object' ← historical bug |
symbol |
Symbol('id') |
'symbol' |
object |
{}, [], function () {}, new Date() |
'object' / 'function' |
The first seven are primitives: immutable values compared by value. object covers everything else — plain objects, arrays, functions, dates, regexes, Map, Set, promises — all compared by reference.
typeof 42; // => 'number'typeof 'hi'; // => 'string'typeof true; // => 'boolean'typeof undefined; // => 'undefined'typeof Symbol(); // => 'symbol'typeof 10n; // => 'bigint'typeof {}; // => 'object'typeof []; // => 'object' (arrays are objects)typeof function () {} // => 'function' (functions are objects, but get their own tag)typeof null; // => 'object' (a bug from 1995, never fixed)typeof has one genuinely useful property: it does not throw on an undeclared identifier.
typeof neverDeclared; // => 'undefined' (no ReferenceError)neverDeclared; // ReferenceError: neverDeclared is not definedDistinguishing object subtypes
Section titled “Distinguishing object subtypes”typeof is too coarse for objects. Use these:
Array.isArray([]); // => truevalue instanceof Date; // => true for dates (same realm only)Object.prototype.toString.call(new Map()); // => '[object Map]'Numbers
Section titled “Numbers”Every JavaScript number is a 64-bit IEEE-754 double-precision float. There is no separate integer type. Integers are exact up to 2^53 − 1; above that, precision is lost.
Number.MAX_SAFE_INTEGER; // => 9007199254740991 (2**53 - 1)Number.MAX_SAFE_INTEGER + 1; // => 9007199254740992Number.MAX_SAFE_INTEGER + 2; // => 9007199254740992 ← wrong, silentlyNumber.isSafeInteger(2 ** 53); // => falseBecause the format is binary, decimal fractions like 0.1 cannot be represented exactly — the same reason 1/3 cannot be written exactly in decimal.
0.1 + 0.2; // => 0.300000000000000040.1 + 0.2 === 0.3; // => false(0.1 + 0.2).toFixed(2); // => '0.30' (a string)Compare floats with a tolerance, or work in integers (store money as cents):
const nearlyEqual = (a, b, eps = Number.EPSILON) => Math.abs(a - b) < eps;nearlyEqual(0.1 + 0.2, 0.3); // => trueLiterals
Section titled “Literals”255; // decimal0xff; // hex => 2550b1111_1111; // binary with numeric separators => 2550o377; // octal => 2551e3; // exponent => 10001_000_000; // separators are ignored by the parserSpecial numeric values
Section titled “Special numeric values”1 / 0; // => Infinity-1 / 0; // => -Infinity0 / 0; // => NaNMath.sqrt(-1); // => NaNNaN (“not a number”) is the result of a failed numeric operation. It is the only value in JavaScript that is not equal to itself.
NaN === NaN; // => falseNumber.isNaN(NaN); // => true ← use thisisNaN('hello'); // => true ← global isNaN coerces first; avoid itNumber.isNaN('hello'); // => false (it is a string, not NaN)There are also two zeros, 0 and -0. They compare equal with === but differ under Object.is:
0 === -0; // => trueObject.is(0, -0); // => falseObject.is(NaN, NaN); // => trueConverting to number
Section titled “Converting to number”Number('42'); // => 42Number('42px'); // => NaN (strict: whole string must be numeric)Number(''); // => 0 (surprising, but defined)parseInt('42px', 10); // => 42 (lenient: reads a prefix)parseFloat('3.14 is pi'); // => 3.14+'42'; // => 42 (unary plus, same as Number())BigInt
Section titled “BigInt”BigInt (ES2020) is an arbitrary-precision integer type for values beyond the safe-integer range — IDs from databases, cryptographic values, high-precision counters.
Create one with an n suffix or BigInt():
const big = 9007199254740993n;const also = BigInt('9007199254740993');big + 1n; // => 9007199254740994n (exact)2n ** 64n; // => 18446744073709551616nBigInts are integers only, and they cannot be mixed with numbers in arithmetic:
1n + 1; // TypeError: Cannot mix BigInt and other types1n + BigInt(1); // => 2nNumber(9n) + 1; // => 10 (explicit conversion is fine)
7n / 2n; // => 3n (truncates toward zero — no fractions)Comparison across types does work, because == and < are defined for it:
1n == 1; // => true (loose equality compares mathematical values)1n === 1; // => false (different types)2n > 1; // => trueStrings
Section titled “Strings”Strings are immutable sequences of UTF-16 code units. Single and double quotes are identical in meaning; pick one and be consistent.
const a = 'single';const b = "double";const c = 'it\'s escaped';const d = "it's easier";Strings never change in place — every “modifying” method returns a new string.
let s = 'hello';s.toUpperCase(); // => 'HELLO's; // => 'hello' (unchanged)s[0] = 'H'; // ignored in sloppy mode; TypeError in strict modes; // => 'hello'Template literals
Section titled “Template literals”Backticks give interpolation, multi-line strings, and no escaping of quotes.
const name = 'Ada';const age = 36;
const msg = `${name} is ${age} years old.Next year: ${age + 1}.`;Ada is 36 years old.Next year: 37.Any expression works inside ${} — function calls, ternaries, nested templates. Newlines and indentation inside backticks are preserved literally, which matters when generating output.
Tagged templates call a function with the literal parts and the interpolated values. This is how libraries build safe SQL or styled components.
function highlight(strings, ...values) { return strings.reduce((out, str, i) => out + str + (i < values.length ? `[${values[i]}]` : ''), '');}
highlight`Hi ${name}, you are ${age}`; // => 'Hi [Ada], you are [36]'Common string methods
Section titled “Common string methods”' padded '.trim(); // => 'padded''a-b-c'.split('-'); // => ['a', 'b', 'c']'abc'.includes('b'); // => true'abc'.startsWith('a'); // => true'abc'.at(-1); // => 'c' (negative index; ES2022)'abc'.slice(1); // => 'bc''5'.padStart(3, '0'); // => '005''ab'.repeat(3); // => 'ababab''a-b'.replaceAll('-', '+'); // => 'a+b' (ES2021)'Ada'.localeCompare('Bob'); // => -1 (locale-aware ordering)Unicode caveat
Section titled “Unicode caveat”.length counts UTF-16 code units, not characters. Emoji and many non-Latin scripts use two units (a surrogate pair).
'café'.length; // => 4'👋'.length; // => 2[...'👋'].length; // => 1 (spread iterates by code point)Use spread or for..of when you need real characters. For grapheme clusters (like flags or family emoji), use Intl.Segmenter.
Boolean
Section titled “Boolean”Only true and false. Conversion follows the truthy/falsy rules covered in operators and control flow; the eight falsy values are false, 0, -0, 0n, '', null, undefined, and NaN.
Boolean(''); // => falseBoolean('0'); // => true (non-empty string)Boolean([]); // => true (every object is truthy)!!'text'; // => true (double negation, an idiomatic cast)null vs undefined
Section titled “null vs undefined”Both mean “no value”, and the difference is intent:
undefined— the engine’s absence. A declared-but-unassigned variable, a missing parameter, a missing property, or a function with noreturn.null— the programmer’s absence. You deliberately set it to say “empty on purpose”.
let x;x; // => undefined({}).missing; // => undefined(function () {})(); // => undefined (no return)
let selected = null; // "nothing is selected right now"They are loosely equal to each other and to nothing else:
null == undefined; // => truenull === undefined; // => falsenull == 0; // => false (null does not coerce to a number here)Defaults treat them differently — a parameter default fires on undefined only:
function greet(name = 'friend') { return `Hi ${name}`; }greet(undefined); // => 'Hi friend'greet(null); // => 'Hi null' ← null is a real valueSymbols
Section titled “Symbols”A symbol is a unique, immutable primitive used as a property key that can never collide with another key. Created with Symbol(description); the description is only a debugging label.
const a = Symbol('id');const b = Symbol('id');a === b; // => false (always unique)a.description; // => 'id'Symbol-keyed properties are skipped by Object.keys, JSON.stringify, and for..in, which makes them useful for metadata that should not leak into serialization.
const SECRET = Symbol('secret');const user = { name: 'Ada', [SECRET]: 'hidden' };
Object.keys(user); // => ['name']JSON.stringify(user); // => '{"name":"Ada"}'user[SECRET]; // => 'hidden'Object.getOwnPropertySymbols(user); // => [Symbol(secret)]Well-known symbols let you hook into language behavior. The most useful is Symbol.iterator, which makes an object work with for..of and spread — see arrays and iteration.
const range = { from: 1, to: 3, [Symbol.iterator]() { let n = this.from, last = this.to; return { next: () => (n <= last ? { value: n++, done: false } : { value: undefined, done: true }) }; },};
[...range]; // => [1, 2, 3]Symbol.for(key) uses a global registry, so the same key returns the same symbol across your whole program (and across realms like iframes):
Symbol.for('app.id') === Symbol.for('app.id'); // => trueDeclaring variables: let, const, var
Section titled “Declaring variables: let, const, var”Three keywords. Use const by default, let when you must reassign, and var essentially never.
const MAX = 100; // cannot be reassignedlet count = 0; // can be reassignedcount += 1;var legacy = 'x'; // function-scoped, hoisted — avoidvar |
let |
const |
|
|---|---|---|---|
| Scope | function | block | block |
| Hoisted | yes, initialized to undefined |
yes, but in the TDZ | yes, but in the TDZ |
| Redeclarable in same scope | yes | no | no |
| Reassignable | yes | yes | no |
| Creates a global object property at top level | yes | no | no |
const does not mean immutable
Section titled “const does not mean immutable”const freezes the binding, not the value. A const object can still be mutated.
const config = { debug: false };config.debug = true; // fine — the object changed, the binding did notconfig = {}; // TypeError: Assignment to constant variableTo make the value itself unchangeable, use Object.freeze — see objects and prototypes.
Block scope
Section titled “Block scope”A block is anything between { } — an if body, a loop body, or a bare block. let and const are visible only inside it; var leaks to the enclosing function.
function demo() { if (true) { var v = 'var'; let l = 'let'; } console.log(v); // => 'var' console.log(l); // ReferenceError: l is not defined}This is the classic loop bug. With var, the loop has one shared binding; with let, each iteration gets a fresh one:
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);// => 3, 3, 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);// => 0, 1, 2Hoisting and the temporal dead zone
Section titled “Hoisting and the temporal dead zone”Hoisting means declarations are processed before any code in the scope runs. What differs is what the name holds before its line executes.
vardeclarations are hoisted and initialized toundefined.- Function declarations are hoisted completely — you can call them before they appear.
let,const, andclassare hoisted but left uninitialized. Touching them before their declaration throws. That window is the temporal dead zone (TDZ).
console.log(hoistedVar); // => undefined (declared, not yet assigned)var hoistedVar = 1;
hoistedFn(); // => 'works' (whole function is available)function hoistedFn() { return 'works'; }
console.log(tdz); // ReferenceError: Cannot access 'tdz' before initializationlet tdz = 1;The TDZ is a feature: it converts a silent undefined into a loud error at the exact line of the mistake.
Automatic semicolon insertion
Section titled “Automatic semicolon insertion”The parser inserts semicolons where a statement clearly ended at a line break. This is why code without semicolons usually works — and why it occasionally does not. Five line starts are dangerous: (, [, `, +, -, and /.
const a = 1const b = a;[1, 2].forEach((n) => console.log(n))Without that leading semicolon, const b = a followed by [1, 2] parses as a[1, 2] — an index access, not a new statement.
The reverse trap: return is always terminated at a line break.
function broken() { return { ok: true };}broken(); // => undefined — ASI inserted a semicolon after `return`function fixed() { return { ok: true, };}Strict mode
Section titled “Strict mode”'use strict' at the top of a file or function opts into a stricter dialect. It turns silent failures into errors and removes some legacy behaviors.
'use strict';
undeclared = 5; // ReferenceError (without strict mode this creates a global)What strict mode changes:
- Assigning to an undeclared variable throws instead of creating a global.
- Assigning to a non-writable property, or to a getter-only property, throws instead of failing silently.
thisin a plain function call isundefinedinstead of the global object.- Duplicate parameter names and octal literals like
010are syntax errors. deleteon a plain variable is a syntax error.evalgets its own scope and cannot leak variables into the caller.
Key points
Section titled “Key points”- Eight types: seven primitives (
number,bigint,string,boolean,undefined,null,symbol) plusobject.typeof nullis'object'— a permanent historical bug. - All numbers are 64-bit floats. Integers are exact only up to
Number.MAX_SAFE_INTEGER;0.1 + 0.2 !== 0.3. UseBigIntfor large exact integers, and never mix it withnumber. - Strings are immutable UTF-16;
.lengthcounts code units, not characters. Template literals handle interpolation and multi-line text. undefinedis the engine’s absence,nullis yours.value == nulltests for both.- Symbols are unique keys invisible to
Object.keysandJSON.stringify. constby default,letwhen reassigning, nevervar.constlocks the binding, not the object.varis function-scoped and hoisted toundefined;let/constare block-scoped with a TDZ that throws on early access.- ASI is why a line starting with
(or[needs a guard, and whyreturnmust never be followed by a line break. - ES modules and classes are strict mode by default.