← Fundamentals
Guided lesson 5 questions · 6 languages

Scope and shadowing

Which declaration a name means, and where a declaration ends.

A name refers to the nearest declaration around the place where it is written. An inner declaration with the same name hides the outer one until its scope ends; it does not change the outer one.

Before you begin

Why this matters

When a variable never seems to change, or a function uses a value you did not expect, the cause is often a name that means a different declaration than you thought. Once you can find the declaration for each name, you can predict these results before you run the program.

What this helps you reason about

  • Find the declaration a name refers to: look first where the name is written, then in the code around it.
  • Predict that a function uses the names around its definition, not the variables of the code that calls it.
  • Spot an assignment that goes to an inner variable and leaves the outer one unchanged.

Every name in a program refers to one declaration. This lesson shows how TypeScript finds it: it looks first where the name is written, then in the code around it. It also shows what happens when two declarations share a name.

Already know this? The short version
  • A name refers to the nearest declaration. Look in this order: its block, then enclosing blocks and functions, then the top level.
  • Scope is lexical: a function uses the variables around its definition, never the local variables of its caller.
  • let and const belong to their block, including if and for bodies; var belongs to the whole function. A let or const covers its whole block, so using the name above its declaration is a compile error.
  • An inner declaration with the same name, including a parameter, shadows the outer one. Assignments to that name inside its scope go to the inner variable, and the outer one keeps its value.

Go to the final check· Go to the practice tickets

Words in this lesson (5)
scope
The part of the program where a name can be used. For let and const, it is the nearest pair of braces, called a block.
block
A group of statements between braces, { … }. An if runs the block after its condition only when the condition is true.
lexical scope
The rule that a name refers to a declaration in the code around the place where the name is written: its own block, an enclosing block or function, or the top level of the file. Which code calls a function makes no difference.
shadowing
Declaring a variable in an inner block with the same name as an outer one. The inner variable hides the outer one until the block ends.
var
The older JavaScript keyword for declaring a variable. A var belongs to the whole function around it, not to a block.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Finding a name

Which declaration does a name refer to?

Variables showed that a declaration inside a block hides an outer one with the same name until the block ends. This lesson asks the question underneath: when the program uses a name, which declaration does it mean?

To find the answer, start at the place where the name is written. Look first in its own block, then in each enclosing block and function, then the top level of the file. The first declaration found is the one the name refers to.

This rule is called lexical scope. Lexical means it depends on the text of the program: where the code is written decides, and the code that calls it does not. Below, main declares its own price and then calls tacoLine. tacoLine is written at the top level, so its price is the top-level one.

main.ts
const price = 4;

function tacoLine(): string {
  return "taco " + price;
}

function main(): void {
  const price = 9;
  console.log(tacoLine()); // taco 4
}

A function uses the names around its own definition, not the variables of the code that calls it.

Precisely: functions inside functions

A function written inside another function can use that function’s variables, because they surround its definition. It keeps using them after the outer call has returned; such a function is called a closure, and a later lesson follows closures in detail.

Experiment

Find the price a function reads

tacoLine is written at the top level. When it uses price, TypeScript looks in tacoLine’s own body, then at the top level, and finds 4 there. main’s price is inside main, which does not surround tacoLine, so tacoLine can never see it. The panel lists, for each frame, the names that code can reach. The stepper sometimes stops on the same line twice; that is normal for this debugger.

Why it matters To predict what a function reads, look at the code around its definition. The code that calls it does not matter.

Predict both lines. Then delete the top-level price, so the only price left is the one in main, and predict again.

const price = 4;

function tacoLine(): string {
  return "taco " + price;
}

function main(): void {
  const price = 9;
  console.log(tacoLine());
  console.log("special " + price);
}

main();
What to look for

At the first stop in main, on line 8, main’s price shows not initialized yet: it belongs to main’s whole body, but line 8 has not run. Step into tacoLine: its frame shows price = 4, main’s frame shows price = 9, and the run prints taco 4 and special 9. After the edit, nothing runs: the compiler reports Cannot find name 'price'. for the line in tacoLine.

Expected output

taco 4
special 9

02 / Blocks

Which parts of a program start a new scope?

In TypeScript, let and const belong to the nearest block. That includes the body of an if, the body of a loop, and a function body. A variable declared in a for loop’s header, such as i in for (let i = 0; i < 3; i++), belongs to the loop and ends with it. A function’s parameters belong to the function’s body.

The older keyword var does not follow blocks. A var belongs to the whole function around it, so a var declared inside an if is the same variable as a var with the same name anywhere else in that function. Modern code uses let and const; a later lesson on TypeScript var and hoisting covers var in full.

main.ts
for (let i = 0; i < 3; i++) {
  const doubled = i * 2;
  console.log(doubled);
}
// console.log(i); // Rejected: i belongs to the loop.

let and const end at the closing brace of their block; var ends with its function.

Coming from older JavaScript: Code written with var can read a loop’s counter after the loop. With let, the same read is a compile error, because the counter belongs to the loop.

03 / Shadowing

Where does an assignment go when a name is hidden?

A declaration inside a block with the same name as an outer one shadows it. Inside that block, the name means the inner variable, and every assignment to the name goes to the inner variable. When the block ends, the inner variable is gone, and the outer one still holds what it held before.

The loop below is meant to keep the highest price in best. The let inside the if declares a second best, so the assignment never reaches the outer one. Removing let turns the line into an assignment to the outer best.

A parameter is a declaration too. A parameter with the same name as an outer variable hides it in the whole function body.

main.ts
let best = 0;
for (const price of [4, 9, 3]) {
  if (price > best) {
    let best = price; // a second best
  }
}
console.log(best); // 0

Inside a shadowing block, assignments go to the inner variable; the outer one is untouched.

Precisely: tools that report shadowing

The TypeScript compiler accepts shadowing without a warning. The no-shadow rule of the ESLint code checker reports it, and many projects turn that rule on.

Experiment

Keep the best price in a loop

let best inside the if declares a second variable that exists only in that block. The assignment of price goes to it. The outer best, which line 4 and the last line read, is never assigned. So 3 counts as a “new best”: line 4 compares it with the outer best, which is still 0. On line 5 the inner best has no value yet. The panel shows not initialized yet there on the first pass, and the value from the previous pass after that; read line 6 instead.

Why it matters When a loop’s result never changes, look for a let or const with the same name inside the loop.

Predict the last line. Then change let best = price; to best = price; and predict again.

const prices = [4, 9, 3];
let best = 0;
for (const price of prices) {
  if (price > best) {
    let best = price;
    console.log("new best", best);
  }
}
console.log("best", best);
What to look for

At each stop on line 6, best shows the inner best: 4, then 9, then 3. Back on line 3, best shows the outer best, still 0, and the run ends with best 0. After the edit, the run prints new best 4, new best 9, and best 9.

Expected output

new best 4
new best 9
new best 3
best 0

04 / Rules

Does every language allow shadowing?

TypeScript allows a declaration to shadow any outer one: in an inner block, in a function written inside another function, or as a parameter. It refuses two let or const declarations of one name in the same block.

One rule surprises readers from other languages. A let or const covers its whole block, from the opening brace, including the lines above the declaration. So once a block declares price, every price in that block means the new one. Using it above the declaration is a compile error. Above the declaration, price never means the outer price.

main.ts
const price = 4;
function special(): number {
  // Rejected: the price below covers this line too.
  const doubled = price * 2;
  const price = 6;
  return doubled;
}

A name declared in a block means that declaration everywhere in the block, even above it.

Coming from Java: Java refuses a local variable that hides another local. TypeScript accepts it without a warning, so the compiler will not catch a shadowing mistake for you.

Coming from C: In C, int price = price + 1; in an inner block reads the new, uninitialized price. In TypeScript the same line is a compile error, for the same reason: the new name already covers its own declaration.

Precisely: the temporal dead zone

TypeScript reports this before the program runs. Plain JavaScript throws a ReferenceError when the line runs instead. The part of the block before the declaration is the temporal dead zone mentioned in Variables. The stepper shows a let or const there as not initialized yet.

Experiment

Declare a name below its first use

A const or let covers its whole block, from the opening brace. After the edit, every price in special’s body means the new one, including line 4, where it has no value yet. On line 4, price never means the top-level price.

Why it matters Once a block declares a name, the outer declaration is hidden in the whole block, even on the lines above.

Predict what is printed. Then add the line const price = 6; between const doubled = price * 2; and return doubled; and predict again.

const price = 4;

function special(): number {
  const doubled = price * 2;
  return doubled;
}

console.log(special());
What to look for

At the stop on line 5, special’s frame shows doubled = 8 and price = 4. The run prints 8. After the edit, nothing runs: the compiler reports Block-scoped variable 'price' used before its declaration., pointing at line 4.

Expected output

8

05 / Try it

Can you explain a fresh example?

Find the declaration for each use of a name before you predict. Start from where the name is written, not from where the code is called.

main.ts
let count = 1;

function add(): void {
  count = count + 10;
}

function run(): number {
  let count = 5;
  {
    let count = 7;
    add();
  }
  return count;
}

console.log(run(), count);

What is printed?

Reveal the reasoning

5 11. add is written at the top level, so its count is the top-level one: 1 becomes 11. Calling add from inside run’s inner block does not change which count add means. run returns its own count, 5, which nothing assigned. The count of 7 is never used.

Practice tickets

  1. SCOPE-101 · Egg boxes with cracked eggs are labelled “ok” → Free
  2. SCOPE-102 · The seat screen shows the wrong number of plates ahead → Sign in · Pro
  3. SCOPE-103 · Some spice sales get the wrong packing fee → Sign in · Pro

Keep exploring

Equality and comparison →

Primary references