VDone Demo VDone Demo
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)

Nikolay Tuzov

Backend Developer
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)
  • 初识 TypeScript

  • TypeScript 常用语法

    • Basic Types
    • Variable Declarations
      • var Declarations
        • Scoping Rules
        • Variable Capturing Quirks
      • let Declarations
        • Block Scoping
        • Redeclarations and Shadowing
        • Block-Scoped Variable Capturing
      • const Declarations
      • let vs. const
      • Destructuring
        • Array Destructuring
        • Object Destructuring
        • Property Renaming
        • Default Values
        • Function Declarations
      • Spread
    • Interfaces
    • Classes
    • Functions
    • Generics
    • Type Inference
    • Advanced Types
  • ts-axios 项目初始化

  • ts-axios 基础功能实现

  • ts-axios 异常情况处理

  • ts-axios 接口扩展

  • ts-axios 拦截器实现

  • ts-axios 配置化实现

  • ts-axios 取消功能实现

  • ts-axios 更多功能实现

  • ts-axios 单元测试

  • ts-axios 部署与发布

  • 《TypeScript 从零实现 axios》
  • TypeScript 常用语法
HuangYi
2020-01-05
Contents

Variable Declarations

# Variable Declarations

let and const are relatively new ways to declare variables in JavaScript. let is similar to var in many ways, but helps avoid some common pitfalls in JavaScript. const is an enhancement over let that prevents reassignment of a variable.

Since TypeScript is a superset of JavaScript, it naturally supports let and const. Below we will elaborate on these new declaration methods and why they are recommended over var.

If you're already well aware of the quirks of var declarations, you can easily skip this section.

# var Declarations

In the ES5 era, we always defined JavaScript variables using the var keyword:

var a = 10
1

As we all know, this defines a variable named a with the value 10.

We can also define variables inside functions:

function f() {
  var message = 'Hello World!'

  return message
}
1
2
3
4
5

And we can access the same variable inside other functions:

function f() {
  var a = 10
  return function g() {
    var b = a + 1
    return b
  }
}

var g = f()
g() // returns 11
1
2
3
4
5
6
7
8
9
10

The above example is a classic closure scenario -- g can access the variable a defined in f. Every time g is called, it can access a from f. Even when g is called after f has finished executing, it can still access a.

# Scoping Rules

var declarations have some strange scoping rules. Consider the following example:

function f(shouldInitialize) {
  if (shouldInitialize) {
    var x = 10
  }

  return x
}

f(true)  // returns '10'
f(false) // returns 'undefined'
1
2
3
4
5
6
7
8
9
10

Some of you may need to look at this example several times. The variable x is defined inside the if statement, yet we can access it outside the statement. This is because var declarations use function scope -- function parameters also use function scope.

These scoping rules can cause several types of errors. One problem is that declaring the same variable multiple times does not produce an error:

function sumMatrix(matrix) {
  var sum = 0
  for (var i = 0; i < matrix.length; i++) {
    var currentRow = matrix[i]
    for (var i = 0; i < currentRow.length; i++) {
      sum += currentRow[i]
    }
  }

  return sum
}
1
2
3
4
5
6
7
8
9
10
11

It's easy to spot the problem here -- the inner for loop overwrites the variable i because all references to i point to the same function-scoped variable. Experienced developers know well that these issues can slip through code review and cause endless trouble.

# Variable Capturing Quirks

Take a guess at what the following code will output:

for (var i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(i)
  }, 100 * i)
}
1
2
3
4
5

The answer is that setTimeout will execute a function after a delay of several milliseconds (waiting for other code to finish):

10
10
10
10
10
10
10
10
10
10
1
2
3
4
5
6
7
8
9
10

Many JavaScript programmers are familiar with this behavior, but if you're confused, you're not alone. Most people expect the output to be:

0
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
10

Every function expression we pass to setTimeout actually refers to the same i from the same scope.

Let's take a moment to think about why. setTimeout runs a function after some number of milliseconds, and it runs after the for loop has completed. After the for loop finishes, the value of i is 10. So when the function is called, it prints 10.

A common solution is to use an immediately-invoked function expression (IIFE) to capture the value of i at each iteration:

for (var i = 0; i < 10; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i)
    }, 100 * i)
  })(i)
}
1
2
3
4
5
6
7

This odd-looking pattern is already very familiar. The parameter i shadows the for loop's i, but since we gave them the same name, we don't need to change the for loop body code much.

# let Declarations

By now you know that var has some issues, which is exactly why let statements were introduced for declaring variables. Other than the different keyword, let is written the same way as var:

let hello = 'Hello!'
1

The key difference is not in syntax but in semantics, which we'll dive into next.

# Block Scoping

When a variable is declared using let, it uses block scoping. Unlike var-declared variables which can be accessed outside their containing function, block-scoped variables cannot be accessed outside their containing block or for loop.

function f(input: boolean) {
  let a = 100

  if (input) {
    // OK: still accessible
    let b = a + 1
    return b
  }

  // Error: 'b' doesn't exist here
  return b
}
1
2
3
4
5
6
7
8
9
10
11
12

Here we defined 2 variables a and b. a is scoped to the body of f, while b is scoped to the if statement block.

Variables declared in a catch clause have similar scoping rules.

try {
  throw 'Oh no!';
}
catch (e) {
  console.log('Catch it.')
}

// Error: 'e' doesn't exist here
console.log(e)
1
2
3
4
5
6
7
8
9

Another property of block-scoped variables is that they can't be read or written to before they're declared. While these variables are "present" throughout their scope, the region before their declaration is called the temporal dead zone. It simply means we can't access them before the let statement, and fortunately TypeScript can tell us about this.

a++ // TS2448: Block-scoped variable 'a' used before its declaration.
let a
1
2

Note that you can still capture a block-scoped variable before it's declared. However, you can't call that function before the variable's declaration. If targeting ES2015, modern runtimes will throw an error; however, TypeScript currently won't report an error.

function foo() {
  // okay to capture 'a'
  return a
}

// Can't call 'foo' before 'a' is declared
// Runtime should throw an error
foo()

let a
1
2
3
4
5
6
7
8
9
10

For more information on the temporal dead zone, see Mozilla Developer Network (opens new window).

# Redeclarations and Shadowing

We mentioned that with var declarations, it doesn't matter how many times you declare; you only get 1.

function f(x) {
  var x
  var x

  if (true) {
    var x
  }
}
1
2
3
4
5
6
7
8

In the above example, all declarations of x actually refer to the same x, and this is perfectly valid code, but it often becomes a source of bugs. Fortunately, let declarations are not so lenient.

let x = 10
let x = 20 // Error, can't re-declare x in the same scope
1
2

TypeScript doesn't require both declarations to be block-scoped before issuing an error warning.

function f(x) {
  let x = 100 // Error: interferes with parameter declaration
}

function g() {
  let x = 100
  var x = 100 // Error: can't have both declarations of x
}
1
2
3
4
5
6
7
8

It's not that block-scoped variables can never be declared with the same name as a function-scoped variable. Rather, the block-scoped variable just needs to be declared in a distinctly different block.

function f(condition, x) {
  if (condition) {
    let x = 100
    return x
  }

  return x
}

f(false, 0) // returns 0
f(true, 0)  // returns 100
1
2
3
4
5
6
7
8
9
10
11

The act of introducing a new name in a nested scope is called shadowing. It is a double-edged sword -- it can accidentally introduce new problems but can also resolve certain bugs. For example, let's rewrite our earlier sumMatrix function using let.

function sumMatrix(matrix: number[][]) {
  let sum = 0
  for (let i = 0; i < matrix.length; i++) {
    let currentRow = matrix[i]
    for (let i = 0; i < currentRow.length; i++) {
      sum += currentRow[i]
    }
  }

  return sum
}
1
2
3
4
5
6
7
8
9
10
11

This version of the loop produces the correct result because the inner loop's i shadows the outer loop's i.

Generally, shadowing should be avoided because we need to write clear code. However, there are scenarios where it can be useful, and you need to weigh the trade-offs.

# Block-Scoped Variable Capturing

Each time a scope is entered, let creates an environment for the variable. Even after the code within the scope has finished executing, that environment and its captured variables persist.

Recall the earlier setTimeout example -- we eventually needed to use an IIFE to capture the state of i for each for loop iteration. What we were really doing was creating a new variable environment for the captured variable. This was painful, but fortunately you don't need to do this in TypeScript.

When a let declaration appears in a loop body, it has entirely different behavior. Instead of just introducing a new variable environment to the loop, it creates a new scope for each iteration -- essentially the same thing we did with the IIFE. So in the setTimeout example, we can simply use let declarations.

for (let i = 0; i < 10 ; i++) {
  setTimeout(function() {
    console.log(i)
  }, 100 * i)
}
1
2
3
4
5

This outputs the expected result:

0
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
10

# const Declarations

const declarations are another way to declare variables.

const numLivesForCat = 9
1

They are similar to let declarations, but as their name suggests, once assigned, they cannot be changed. In other words, they have the same scoping rules as let, but you cannot reassign them.

This is straightforward -- the value they reference is immutable.

const numLivesForCat = 9
const kitty = {
  name: 'Kitty',
  numLives: numLivesForCat
}

// Error
kitty = {
  name: 'Tommy',
  numLives: numLivesForCat
};

// OK
kitty.name = 'Jerry'
kitty.numLives--
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Unless you take special measures, the internal state of a const variable is still modifiable. Fortunately, TypeScript allows you to set object members as read-only. The Interfaces section has detailed information.

# let vs. const

Now that we have two declaration methods with similar scoping, the natural question is which one to use. Like most broad questions, the answer is: it depends.

Using the principle of least privilege, all variables that you don't plan to modify should use const. The basic rule is that if a variable doesn't need to be written to, then others using the code shouldn't be able to write to it either, and you should think about why these variables would need reassignment. Using const also makes it easier to reason about data flow.

# Destructuring

# Array Destructuring

The simplest form of destructuring is array destructuring assignment:

let input = [1, 2]
let [first, second] = input
console.log(first) // outputs 1
console.log(second) // outputs 2
1
2
3
4

This creates 2 named variables first and second. This is equivalent to using indexing but more convenient:

let first = input[0]
let second = input[1]
1
2

Destructuring works with function parameters:

let input: [number, number] = [1, 2]

function f([first, second]: [number, number]) {
  console.log(first)
  console.log(second)
}

f(input)
1
2
3
4
5
6
7
8

You can create rest variables using the ... syntax in arrays:

let [first, ...rest] = [1, 2, 3, 4]
console.log(first) // outputs 1
console.log(rest) // outputs [ 2, 3, 4 ]
1
2
3

You can also ignore trailing elements you don't care about:

let [first] = [1, 2, 3, 4]
console.log(first) // outputs 1
1
2

Or other elements:

let [, second, , fourth] = [1, 2, 3, 4]
1

# Object Destructuring

You can also destructure objects:

let o = {
    a: 'foo',
    b: 12,
    c: 'bar'
}
let { a, b } = o

1
2
3
4
5
6
7

This creates a and b from o.a and o.b. Note that you can skip c if you don't need it.

You can create rest variables using the ... syntax in objects:

let { a, ...passthrough } = o
let total = passthrough.b + passthrough.c.length
1
2

# Property Renaming

You can give properties different names:

let { a: newName1, b: newName2 } = o
1

The syntax here gets confusing. You can read a: newName1 as "a as newName1". The direction is left-to-right, as if you had written:

let newName1 = o.a
let newName2 = o.b
1
2

Confusingly, the colon here does not indicate the type. If you want to specify the type, you still need to write the full pattern after it.

let {a, b}: {a: string, b: number} = o
1

# Default Values

Default values let you use a fallback when a property is undefined:

function keepWholeObject(wholeObject: { a: string, b?: number }) {
  let { a, b = 1001 } = wholeObject
}
1
2
3

Now, even if b is undefined, the keepWholeObject function's wholeObject variable will have values for both properties a and b.

# Function Declarations

Destructuring also works with function declarations. Consider this simple case:

type C = { a: string, b?: number }
function f({ a, b }: C): void {
  // ...
}
1
2
3
4

But usually it's more common to specify defaults, and getting defaults right with destructuring can be tricky. First, you need to write the pattern before the default value.

function f({ a = '', b = 0 } = {}): void {
  // ...
}
f()
1
2
3
4

The above code is an example of type inference, which will be introduced in later sections.

Second, you need to know how to give a default or optional property on the destructured property to replace the main initializer list. Remember that C's definition has an optional property b:

function f({ a, b = 0 } = { a: '' }): void {
  // ...
}
f({ a: 'yes' }) // OK, default b = 0
f() // OK, default a: '', b = 0
f({}) // Error, 'a' is required once a parameter is passed
1
2
3
4
5
6

Use destructuring with care. As the previous examples show, even the simplest destructuring expressions can be hard to understand. Especially with deeply nested destructuring, it can become truly difficult to understand even without stacking renaming, default values, and type annotations. Try to keep destructuring expressions small and simple.

# Spread

let first = [1, 2]
let second = [3, 4]
let bothPlus = [0, ...first, ...second, 5]
1
2
3

This gives bothPlus the value [0, 1, 2, 3, 4, 5]. The spread operation creates a shallow copy of first and second. They are not modified by the spread.

You can also spread objects:

let defaults = { food: 'spicy', price: '$10', ambiance: 'noisy' }
let search = { ...defaults, food: 'rich' }
1
2

The value of search is { food: 'rich', price: '$10', ambiance: 'noisy' }. Object spreading is more complex than array spreading. Like array spreading, it proceeds left-to-right, but the result is still an object. This means properties that come later in the spread object overwrite earlier properties. So if we modify the above example to spread at the end:

let defaults = { food: 'spicy', price: '$10', ambiance: 'noisy' }
let search = { food: 'rich', ...defaults }
1
2

Then the food property in defaults will overwrite food: 'rich', which is not the result we want here.

Edit (opens new window)
#TypeScript
Last Updated: 2026/03/21, 12:14:36
Basic Types
Interfaces

← Basic Types Interfaces→

Recent Updates
01
How I Discovered Disposable Email — A True Story
06-12
02
Animations in Grid Layout
09-15
03
Renaming a Git Branch
08-11
More Articles >
Theme by VDone | Copyright © 2026-2026 Nikolay Tuzov | MIT License | Telegram
  • Auto
  • Light Mode
  • Dark Mode
  • Reading Mode