TypeScript variable declaration


May 07, 2021 21:00 TypeScript


Table of contents


TypeScript variable declaration

let and const relatively new variable declarations in JavaScript. A s we mentioned earlier, let is similar to var in many ways, but can help you avoid some of the common problems in JavaScript. const is let enhancement to let that prevents a variable from been assigned again.

Because TypeScript is a superset of JavaScript, it supports let const Below we'll explain in detail how these new claims are made and why they are recommended instead var

If you didn't really care when you used JavaScript before, this section will evoke your memories. If you already var the weirdness of var claims, you can easily skip this section.

var declaration

We've always var variables with the var keyword.

var a = 10;

As you can understand, a variable named a of 10 defined here.

We can also define variables within functions:

function f() {
    var message = "Hello, world!";

    return message;
}

And we can also access the same variables inside other functions.

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

var g = f();
g(); // returns 11;

In the example above, g can get the f defined in a function. W henever g is called, it can access the f in a Even if g f after f has been executed, it can still access and modify a .

function f() {
    var a = 1;

    a = 2;
    var b = g();
    a = 3;

    return b;

    function g() {
        return a;
    }
}

f(); // returns 2

Scope rules

For those familiar with other languages, var declares some strange scope rules. Take a look at the following example:

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

    return x;
}

f(true);  // returns '10'
f(false); // returns 'undefined'

Some readers may want to read this example several more times. T he x is defined in the if statement, but we can access it outside the statement. T his var declaration can be accessed anywhere inside the function, module, namespace, or global scope that contains it (we'll cover it in more detail later), and the blocks of code that contain it have little effect on it. S ome people call this the var scope" or "function scope". Function parameters also use function scopes.

These scope rules may cause some errors. One of them is that declaring the same variable multiple times does not result in an error:

function sumMatrix(matrix: number[][]) {
    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;
}

It's easy to see some problems here, and for the middle i i refers to variables within the same function scope. Experienced developers are well aware that these issues can be missed during code reviews, causing endless hassles.

Variables get weird

A quick guess at what the following code will return:

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

As you'll see, setTimeout executes a function after a few milliseconds of delay (waiting for the other code to execute).

Well, take a look at the results:

10
10
10
10
10
10
10
10
10
10

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

0
1
2
3
4
5
6
7
8
9

Remember the variable acquisition we talked about above?

Whenever g is called, it can access the f in a

Let's take a little time to consider the situation in this context. setTimeout a function after a few milliseconds and is at the for the for loop. for end of the i the value of i 10 So when the function is called, it prints 10

A common workaround is to use an immediately executed function expression (IIFE) to capture the value i

for (var i = 0; i < 10; i++) {
    // capture the current state of 'i'
    // by invoking a function with its current value
    (function(i) {
        setTimeout(function() { console.log(i); }, 100 * i);
    })(i);
}

This strange form has become commonplace. Parameter i for in the i but because we have the same name, we don't have to for code in the loop body much.

let declaration

Now that you know there are some problems with var this explains why you use let statement to declare variables. In addition to the different names, let var in the same way as var.

let hello = "Hello!";

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

Block scope

When you declare a variable with let it uses a syn word scope or a block scope. Unlike var with var, which can be accessed outside the function that contains them, block scope variables cannot be for loop that contains them.

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

    if (input) {
        // Still okay to reference 'a'
        let b = a + 1;
        return b;
    }

    // Error: 'b' doesn't exist here
    return b;
}

Here we define two variables a and b a scope of a is inside the f function, while the scope of b is in the if statement block.

catch declared in catch statements also have the same scope rules.

try {
    throw "oh no!";
}
catch (e) {
    console.log("Oh well.");
}

// Error: 'e' doesn't exist here
console.log(e);

Another feature of variables with block-level scopes is that they cannot be read or written before they are declared. A lthough these variables are always "present" in their scope, the regions that were in the time dead zone until the code that declared them were declared. It's just to show that we let before let statements, and fortunately TypeScript can tell us that.

a++; // illegal to use 'a' before it's declared;
let a;

Note that we can still get a block scope variable before it is declared. I t's just that we can't call that function before the variable is declared. If the generated code target is ES2015, the modern runtime throws an error;

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

// 不能在'a'被声明前调用'foo'
// 运行时应该抛出错误
foo();

let a;

For more information about Time Dead Zone, check out the Mozilla Developer Network here .

Redefining and masking

When we mention var declarations, it doesn't matter how many times you declare;

function f(x) {
    var x;
    var x;

    if (true) {
        var x;
    }
}

In the example above, all x actually refer to the same x this is fully valid code. T his often becomes a source of bugs. Well, let it won't be so loose.

let x = 10;
let x = 20; // 错误,不能在1个作用域里多次声明`x`

TypeScript does not require both declarations, which are block-level scopes, to give a false 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'
}

This is not to say that block-level scope variables cannot be declared within the scope of a function. Instead, block-level scope variables need to be declared in unused blocks.

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

    return x;
}

f(false, 0); // returns 0
f(true, 0);  // returns 100

The act of introducing a new name into a nested scope is called masking. I t is a double-edged sword that may accidentally introduce new problems and also resolve some errors. For example, let's say we now override sumMatrix let

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

    return sum;
}

This version of the loop gets the correct result because the i of the i can mask the i of the outer i

In general, we should avoid masking because we need to write clear code. There are also some scenarios that are suitable for using it, and you need to plan it well.

The acquisition of block-level scope variables

When we first talked about getting variables declared with var we briefly explored how variables behave when they are acquired. I ntuitively, each time it enters a scope, it creates a variable environment. Even if the scope code has been executed, the environment and the variables it captures remain.

function theCityThatAlwaysSleeps() {
    let getCity;

    if (true) {
        let city = "Seattle";
        getCity = function() {
            return city;
        }
    }

    return getCity();
}

Because city got city in city we can if statement even after it's executed.

Recalling the previous setTimeout up using a function expression that executes immediately to get the for loop iteration. I n fact, what we're doing is creating a new variable environment for the variables we get. It's painful, but fortunately you don't have to do it in TypeScript.

There let completely different behavior when the let declaration appears in the loop body. N ot only is a new variable environment introduced into the loop, but such a new scope is created for each iteration. That's what we do when we use function expressions that execute setTimeout example we'll just use the let declaration.

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

outputs results that are consistent with expectations:

0
1
2
3
4
5
6
7
8
9

const declaration

const are another way to declare variables.

const numLivesForCat = 9;

They are let declarations, but as their names say, they cannot be changed after being assigned. In other words, they have the same scope rules as let but they cannot be re-assigned.

It's easy to understand that the values they reference are imm changed.

const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}

// Error
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

Unless you use a special method to avoid it, the internal state of the const variable is actually modifiable. F ortunately, TypeScript allows you to set members of an object to read-only. Interface The chapter is detailed.

let vs. const

Now that we have two scope-similar declarations, we naturally ask which one to use. As with most general questions, the answer is: it depends.

Using the principle of least privilege, all variables except those you plan to modify should use const T he basic principle is that if a variable doesn't need to be written to it, then no one else who uses it can write them, and think about why they need to be re-assigned. Using const it easier for us to speculate on the flow of data.

On the other hand, users like let of let. Let is used in most parts of let

Talk to your team members if appropriate, according to your own judgment. F ortunately, TypeScript allows you to specify that members of an object are readonly . The chapter on Interfaces has the details.

Deconstruction

Another TypeScript is already able to parse other ECMAScript 2015 features. S ee the article on the Mozilla Developer Network for a full list. In this chapter, we'll give you a brief overview.

Deconstruct the array

The simplest deconstruction is the deconstruction assignment of an array:

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

This creates two named first and second Equivalent to using an index, but more convenient:

first = input[0];
second = input[1];

Deconstruction is better for declared variables:

// swap variables
[first, second] = [second, first];

Acting on function parameters:

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

You can use ...name syntax creates a list of remaining variables:

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

Of course, since it's JavaScript, you can ignore the trailing elements that you don't care about:

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

or other elements:

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

The object is deconstructed

You can also deconstruct objects:

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

This o.a o.b a b Note that if you don't c you can ignore it.

Just like array deconstruction, you can use unsted assignments:

({a, b} = {a: "baz", b: 101});

Note that we need to enclose it in parentheses, because Javascript typically { to a block with the statement that starts with .

The property is renamed

You can also give properties different names:

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

The grammar here is starting to get messy. Y ou can a: newName1 as a newName1 The direction is from left to right, as if you had written the following:

let newName1 = o.a;
let newName2 = o.b;

Confusingly, the colon here is not an indication type. If you want to specify its type, you still need to write the full pattern later.

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

The default

The default value lets you use the default value when the property is undefined:

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

Now, even b is undefined, the properties a and b of wholeObject a and b have values. keepWholeObject

Function declaration

Deconstruction can also be used for function declarations. Here's a look at the simple case:

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

However, it is often more about specifying default values, which can be tricky to deconstruct. First, you need to know to set the default value before setting its type.

function f({a, b} = {a: "", b: 0}): void {
    // ...
}
f(); // ok, default to {a: "", b: 0}

Second, you need to know that a default or optional property is given on the deconstruction property to replace the main initialization list. To know that the definition of C has a b optional property:

function f({a, b = 0} = {a: ""}): void {
    // ...
}
f({a: "yes"}) // ok, default b = 0
f() // ok, default to {a: ""}, which then defaults b = 0
f({}) // error, 'a' is required if you supply an argument

Be careful with deconstruction. A s you can see from the previous example, even the simplest deconstruction can have many problems. E specially when there is deep nesting deconstruction, the default values and type annotations are incomprehensible even if there are no stacked renames. D econstruct expressions should be kept as small and simple as possible. You can also use the assignment expression that will be generated directly by deconstructing yourself.