The keywords var, let and const declare variables in JavaScript and TypeScript, and they differ mainly in scope and in whether we can assign a new value. They also behave differently when code reads a variable before its declaration line. A var variable is visible in the whole function, whereas let and const are visible only inside the nearest pair of braces, and a const variable cannot be reassigned.
We choose one of the keywords every time we declare a variable, for example a loop counter or a configuration object. In current code, we use const by default, let when the value must change, and var not at all.
The following example shows the six differences that matter in practice, compiled with TypeScript 7.0.2 in strict mode. The runtime errors come from Node.js 22.
// 1. Reassignment
let count = 1;
count = 2; // allowed
const max = 10;
// max = 20; // error TS2588: max is a constant
// 2. Scope
{
var fruit = "apple"; // function scope
let color = "red"; // block scope
}
const seen = fruit; // seen = "apple"
// color; // error TS2304: Cannot find name 'color'
// 3. Use before declaration
// JavaScript: var gives undefined, let and const throw a ReferenceError
// 4. Loop closures
const varFns: (() => number)[] = [];
for (var i = 0; i < 3; i++) varFns.push(() => i);
const letFns: (() => number)[] = [];
for (let j = 0; j < 3; j++) letFns.push(() => j);
const fromVar = varFns.map((f) => f()); // fromVar = [3, 3, 3]
const fromLet = letFns.map((f) => f()); // fromLet = [0, 1, 2]
// 5. A const object can still change
const ages = { Lokesh: 37 };
ages.Lokesh = 38; // allowed, ages = { Lokesh: 38 }
// 6. Inferred types
let name1 = "Raj"; // type string
const name2 = "Raj"; // type "Raj"
Notice that the var loop gives every function the same final value 3, whereas the let loop keeps one value per iteration.
Next, we compare the keywords in detail, with the loop closure bug that let fixes and the reason a const object can still change. We also see the TypeScript compile errors that catch misuse of each keyword, and why a const gets a narrower inferred type than a let.
1. Comparison Table
ES2015 (ES6) added let and const to replace var, whose scope rules caused bugs. The two newer keywords share the same scope rules and differ only in reassignment, whereas var differs from both in almost every row.
| Rule | var | let | const |
|---|---|---|---|
| Scope | Function (or the module/script when declared at the top level) | Block | Block |
| Reassign | Yes | Yes | No |
| Declare again in the same scope | Yes | No | No |
| Must be initialized | No | No | Yes |
| Read before the declaration line | Returns undefined | ReferenceError | ReferenceError |
| Loop variable in a closure | One variable shared by all iterations | A new variable per iteration | A new variable per iteration (for…of) |
| Top-level declaration in a classic browser script | Becomes a property of window | Does not | Does not |
| TypeScript inferred type of “Raj” | string | string | The literal type “Raj” |
2. The var Keyword: Function Scope
A var variable belongs to the whole function that contains it, not to the block where we wrote it, so the braces of an if, a loop or a plain block do not limit it. For example, the checkout() function declares state inside a block and can still return it.
function checkout(): string {
{
var state = "paid";
}
return state;
}
const result = checkout(); // result = "paid"
A var declared outside any function is visible in the whole file. When the file is a classic browser script (not a module), the variable also becomes a property of the global window object, where it can collide with variables of other scripts. For example, a page that loads an analytics script and our own script, both with a top-level var config, ends up with one shared config. Top-level let and const never create window properties.
JavaScript also lets us declare the same var twice, which hides mistakes such as reusing a name for a second purpose. TypeScript accepts a second var only when both declarations have the same type.
var visits = 1;
var visits = 2; // allowed, visits = 2
// var visits = "two"; // error TS2403
The commented line fails because subsequent variable declarations must have the same type, and visits is already a number.
The JavaScript engine creates every var variable of a function before it runs the first line of that function (the behavior is called hoisting), so reading a var before its declaration line returns undefined instead of failing.
3. The let Keyword: Block Scope
A let variable exists only inside the nearest pair of braces, such as a function body, an if block, a loop body or a plain block. A counter of failed login attempts inside a for loop is a typical let variable. A variable with the same name inside a nested block is a different variable that hides the outer one only within that block (the behavior is called shadowing).
let score = 10;
{
let score = 20;
console.log(score); // 20
}
const outer = score; // outer = 10
Code outside the block cannot see a let variable at all, so TypeScript reports error TS2304 (cannot find name) for a use outside the block. When we declare the same name twice in one scope, TypeScript reports error TS2451 (cannot redeclare block-scoped variable).
3.1. The Temporal Dead Zone
A let variable is hoisted as well, but the engine initializes it only when its declaration line runs. The part of the block before that line is called the temporal dead zone, and any read there throws a ReferenceError. The ReferenceError is stricter than the undefined that var returns, so a hidden bug becomes a clear error message.
console.log(early);
let early = 1;
ReferenceError: Cannot access 'early' before initialization
TypeScript reports the same mistake at compile time with error TS2448, because the block-scoped variable early is used before its declaration. In strict mode, the compiler even reports a var that we read before assigning it (error TS2454), so in TypeScript the hoisting difference rarely reaches runtime.
3.2. Loop Variables in Closures
The most practical difference between var and let shows up when a function created inside a loop uses the loop variable. A var loop variable is one variable for the whole loop, so every function sees its final value. A let loop variable is a new variable in every iteration, so each function keeps the value from its own iteration.
const letFns: (() => number)[] = [];
for (let j = 0; j < 3; j++) {
letFns.push(() => j);
}
const fromLet = letFns.map((f) => f()); // fromLet = [0, 1, 2]
With var i instead of let j, the same code returns [3, 3, 3], because the loop ends when i reaches 3 and all three functions read that one variable afterwards. The same bug appears with callbacks such as event handlers or setTimeout() inside a var loop. Before ES2015, developers wrapped the loop body in an extra function to get a new variable per iteration, and let makes that unnecessary.
4. The const Keyword: No Reassignment
A const variable follows the same block scope and temporal dead zone rules as let, with two more rules.
- A const must get its value in the declaration, so const empty; gives error TS1155 (const declarations must be initialized).
- A const cannot be assigned again.
When we assign a const again, TypeScript reports an error at compile time, and plain JavaScript throws a TypeError at runtime.
error TS2588: Cannot assign to 'max' because it is a constant.
TypeError: Assignment to constant variable.
The const keyword protects the variable, not the value. When the value is an object or an array, we can still change its contents, because the variable keeps pointing to the same object. For example, a const cart array still accepts new items with push().
// 1. Object and array contents can change
const ages = { Lokesh: 37, Raj: 35 };
ages.Raj = 36; // allowed
const fruits = ["apple"];
fruits.push("banana"); // fruits = ["apple", "banana"]
// 2. Object.freeze(): read-only at runtime and in the type
const frozen = Object.freeze({ Lokesh: 37 });
// frozen.Lokesh = 38; // error TS2540
// 3. as const: read-only tuple of literal types
const sizes = ["small", "large"] as const; // type readonly ["small", "large"]
// sizes.push("medium"); // error TS2339
To protect the contents, we have two tools.
- Object.freeze() makes an object read-only at runtime, and TypeScript types the result as Readonly, so the commented assignment fails because Lokesh is a read-only property. The freeze is shallow, so nested objects stay writable.
- The TypeScript as const assertion and the readonly modifier work only at compile time. The call sizes.push() fails because the type readonly [“small”, “large”] has no push property.
To get a changed version without touching the original, we create a copy with the TypeScript spread operator.
5. How const Changes the Inferred Type in TypeScript
TypeScript uses the keyword to decide how specific an inferred type should be. A let variable can receive other strings later, so TypeScript widens “large” to the type string. A const variable keeps its value forever, so TypeScript gives it the literal type “large” (that is, a type with only one value).
type Size = "small" | "large";
const picked = "large"; // type "large"
let typed = "large"; // type string
let chosen: Size = "small";
chosen = picked; // allowed, chosen = "large"
// chosen = typed; // error TS2322
The commented line fails because type string is not assignable to type Size, even though typed holds “large” at that moment. To fix it, we use either a const or an explicit annotation such as let typed: Size = “large”. For object properties, a const does not help, because properties can change, so we use as const there.
6. Assigning a Variable Without Any Keyword
In old JavaScript, assigning to a name that was never declared, such as index = 0, created a global variable, so two loops in different functions that both used an undeclared index overwrote each other’s value. Current code no longer creates global variables by accident, because ES modules and classes always run in strict mode, where the assignment throws a ReferenceError. TypeScript also rejects the assignment at compile time.
error TS2304: Cannot find name 'index'.
ReferenceError: index is not defined
Only a classic script without “use strict” still creates the global variable.
7. Which Keyword to Use
In TypeScript and modern JavaScript projects, we follow a short rule.
- Use const for every variable that is assigned once, which covers most variables, including objects and arrays whose contents change.
- Use let for a variable that gets a new value, such as a counter, an accumulator, or a value set in different branches of an if.
- Do not use var in new code. It remains valid only for compatibility with old code.
Linters enforce the rule, because the ESLint rules no-var and prefer-const report every var and every let that is never reassigned. A const also documents intent, so a reader knows at once that the name keeps its value, and TypeScript infers narrower types for it.
8. Example Code and Runtime Errors
Every TypeScript snippet is in the src folder of the var-let-const examples. A separate runtime-errors.js file prints what plain JavaScript does at runtime with var hoisting, the temporal dead zone, const reassignment and an undeclared variable. The project uses the same versions as the snippets, TypeScript 7.0.2 and Node.js 22.
npm install
npm start # compile the TypeScript snippets and run them
npm run js # run js/runtime-errors.js
undefined
ReferenceError: Cannot access 'early' before initialization
TypeError: Assignment to constant variable.
ReferenceError: index is not defined
9. Conclusion
The keywords var, let and const differ mainly in scope and reassignment. A var variable is function-scoped and returns undefined before its declaration, whereas let and const are block-scoped, throw a ReferenceError when read too early, and give each loop iteration its own variable.
A const also forbids reassignment, but it does not freeze objects, so we use either Object.freeze() or as const to protect the contents. In TypeScript, a const also gets a literal type. When we use const first, let when needed and never var, we avoid the scope and closure bugs of var.
10. References
MDN describes the runtime rules of each keyword, and the TypeScript handbook covers how the compiler treats them.
- MDN: var
- MDN: let
- MDN: const
- MDN: Object.freeze()
- TypeScript Handbook: Variable Declarations
- TypeScript Handbook: Literal Types
- ESLint: prefer-const
Happy Learning !!