Capítulo 31 de 36
let/const fix a set of var's well-known JavaScript footguns (function-scoping instead of block-scoping, silent re-declaration, closure-capture-by-reference-not-value in loops) — TypeScript inherits all of this directly from ES2015 semantics, plus type-level destructuring syntax.
var is function-scoped, not block-scoped: a var declared inside an if/for block is visible throughout the entire enclosing function (or module/global scope), not just the block — so reading it right after the block, or even before its own declaration line (hoisted as undefined), doesn't error.var allows silent re-declaration: declaring the same var name multiple times in the same function scope is legal and refers to the same variable — a classic source of accidental variable clobbering, e.g. a nested loop reusing the same loop-counter name as an outer loop by mistake.var + closure-in-a-loop bug: every closure created inside a var-based for loop (e.g. passed to setTimeout) captures the same variable, not a per-iteration snapshot — by the time any of them actually run, the loop has finished and the variable holds its final value. The traditional workaround is an IIFE per iteration to force a fresh binding.let is block-scoped: visible only within its nearest enclosing block (if, for, {}, catch), not leaking to the surrounding function. This alone eliminates the accidental-reuse class of bug.let's temporal dead zone (TDZ): a block-scoped variable exists (in a compiler sense) throughout its whole scope but can't be read or written before its declaration line — accessing it earlier is a compile error in TypeScript (and a runtime error in real ES2015+ engines). Capturing a let in a closure before its declaration is legal syntactically, but actually calling that closure before the declaration runs is illegal.let forbids re-declaration in the same scope (let x; let x; errors), including across var/let mixing in ways that would otherwise conflict — much stricter than var's "just merges into the same variable" behavior.let in a nested block can reuse an outer name without conflict (a new, distinct binding) — this is what makes a let-based nested loop with the same counter name in both loops behave correctly, unlike the equivalent var version, which silently clobbers the outer counter.let in a loop creates a fresh binding per iteration: this is exactly what fixes the classic setTimeout-in-a-loop bug without needing an IIFE — each iteration's closure captures its own let i, not a single shared variable.const: same block-scoping and TDZ rules as let, but forbids reassignment of the binding itself. This is not deep immutability — a const object's own properties remain freely mutable (const obj = {...}; obj.field = "x" is fine; obj = {...} is not). Use readonly properties (or Readonly<T>/as const) when actual property-level immutability is the goal.let vs. const guidance: default to const for anything not intentionally reassigned (principle of least privilege — makes data flow easier to reason about and signals intent to readers); reach for let only for variables genuinely meant to change.const [a, b] = pair) and object destructuring (const { x, y } = point) both work in TypeScript exactly as in JS, plus optional type annotations on the whole pattern (annotations can't go inside individual destructured names — see Object Types for why). Supports rest elements (const [first, ...rest] = list), default values for missing/undefined fields, renaming during object destructuring, and destructuring directly in function parameter lists.// The classic var-in-a-loop bug and its let fix:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // logs 3, 3, 3 — one shared `i`
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // logs 0, 1, 2 — fresh `j` per iteration
}
let's per-iteration binding eliminating the shared-variable closure bug that var requires an IIFE to work around.const point = { x: 1, y: 2 };
point.x = 5; // OK — const only locks the binding, not the object's contents
point = { x: 0, y: 0 }; // Error — can't reassign a const
const preventing rebinding while leaving the referenced object's own mutability untouched.| Feature | var | let | const |
|---|---|---|---|
| Scope | function | block | block |
| Re-declarable in same scope | yes | no | no |
| Temporal dead zone | no (hoisted as undefined) | yes | yes |
| Reassignable | yes | yes | no |
| Fresh binding per loop iteration | no | yes | n/a (loop var can't be const-reassigned) |
const; use let only where reassignment is actually intended; avoid var in new code entirely — its function-scoping and re-declaration permissiveness are pure liability in modern TypeScript.const is shallow — it locks the binding, not the referenced value's contents; reach for readonly/Readonly<T>/as const for real immutability.var-in-a-loop closure bug (setTimeout printing the same final value repeatedly) simply doesn't happen with let — no IIFE workaround needed.as const for freezing literal types, readonly for property-level immutability.