Объявление переменных в TypeScript

В JavaScript существует два относительно новых способа объявления переменных — с помощью ключевых слов let и const. Объявление переменной с использованием let очень похоже на объявление с использованием var, но позволяет избегать некоторых распространенных подводных камней при работе с JavaScript. Объявление с помощью const делает невозможным все последующие присваивания новых значений переменной.

Язык TypeScript полностью поддерживает объявление переменных с помощью let и const, поскольку является надмножеством языка JavaScript. Далее будет рассмотрено, почему let и const являются более предпочтительным способом объявления переменных, нежели var.

Объявление с помощью var

Использование ключевого слова var для объявления переменной является традиционным способом в языке JavaScript.

var a = 10;

Как можно видеть, здесь мы объявили переменную с именем a и присвоили ей значение, равное 10.

Также объявлять переменную можно внутри функции:

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

Ну и конечно можно получить доступ к этим переменным внутри других функций:

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

В приведенном выше примере, функция g захватывает переменную а, объявленную в функции f. Когда будет вызвана функция g, значение переменной a в g будет привязано к значению a из f. Таким образом даже при однократном вызове функции g в f, g получает доступ к переменной a и может изменить её значение.

function f() {
var a = 1;
    a = 2;
var b = g();
a = 3;
    return b;
    function g() {
return a;
}
}
f(); // returns '2'

Правила области видимости

Объявления переменных с использованием var имеют несколько необычные правила области видимости по сравнению с другими языками программирования. Рассмотрим пример:

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

return x;
}

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

В данном примере переменная x объявляется внутри блока if, однако мы можем получить к ней доступ вне его. Происходит это потому, что объявления с помощью var доступны в любом месте в пределах Lсодержащей их функции, модуля, пространства имен, или в глобальном масштабе независимо от содержащего блока. Иногда это называют var-областью видимости (var-scoping) или областью видимости функции. Параметры, передаваемые в функцию, также принадлежат области видимости функции.

Данные правила могут быть причиной нескольких типов ошибок. Одна из проблем — это отсутствие сообщений об ошибках при многократном объявлении одной и той же переменной:

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;
}

В данном примере объявление переменной во внутреннем цикле for затрет значение переменной i во внешнем цикле, поскольку i находится в пределах одной и той же области видимости функции. Подобные случаи могут ускользнуть от внимания разработчиков и, в конечном итоге, привести к трудно отлавливаемым ошибкам.

Особенности захвата переменных

Рассмотрим следующий пример. Каким будет результат работы данного куска кода?

for (var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
Если вы еще не знаете, то setTimeout выполняет функцию, переданную в качестве первого аргумента спустя некоторое количество миллисекунд, переданное во втором аргументе.

Так каким же будет результат?

10
10
10
10
10
10
10
10
10
10

Для множества JavaScript разработчиков такое поведение будет вполне ожидаемым, однако если для вас это сюрприз, то вы в любом случае не одни. Казалось бы, стоило ожидать следующего результата:

0
1
2
3
4
5
6
7
8
9

Что же здесь не так? На самом деле каждая функция, которую мы передаем в setTimeout ссылается на одну и ту же переменную i из одной и той же области видимости.

setTimeout выполнит функцию спустя некоторое количество миллисекунд, но это будет уже после того, как цикл завершится. И в этом случае значение переменной i будет равно 10.

Выходом из этой ситуации является использование “функции немедленного вызова” (IIFE — an Immediately Invoked Function Expression) для захвата переменной 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);
}

Такой странный шаблон достаточно распространен в JavaScript. Параметр i закрывает” переменную i, объявленную в цикле, а поскольку имена переменной и параметра функции совпадают, мы больше не можем изменить переменную цикла.

Объявления с помощью let

Именно из-за проблем, присущим объявлению переменных с использованием var, было введено объявление с использованием let.

let hello = "Hello!";

Ключевым отличием здесь является не синтаксис, а семантика объявления, которое сейчас и будет рассмотрено.

Область видимости на уровне блока

Когда переменная объявляется с помощью ключевого слова let, считается, что она находится в так называемой лексической области видимости или в области видимости блока. В отличие от var, let переменные не видны вне блока, в котором они были объявлены.

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;
}

В данном примере переменная а принадлежит области видимости функции f, а область видимости переменной b ограничена блоком if. Поэтому попытка получить доступ к переменной b вне блока if приведет к ошибке.

Переменные, определенные в блоке catch, не доступны за пределами этого блока, что похоже на объявление переменных с помощью let.

try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
// Error: 'e' doesn't exist here
console.log(e);

К переменным, определенным с помощью ключевого слова let, нельзя обращаться до места их непосредственного объявления. Хотя переменная и существует в определенной области видимости, все попытки обращения к ней до объявления находятся в так называемой “временной мертвой зоне”.

a++; // нельзя обращаться к переменной до объявления
let a;

Стоит отметить, что сейчас можно захватить let-переменную до ее объявления. Единственная проблема в том, что нельзя вызывать функцию до объявления этой переменной. Если используется ES2015, то будет сгенерировано исключение; однако, на данный момент TypeScript не выдаст никакого сообщения об ошибке.

function foo() {
// Допустимо
return a;
}

// нельзя вызывать 'foo' до того, как 'a' будет объявлена
// будет брошено исключение во время выполнения
foo();

let a;

Более подробно о “временных мертвых зонах” можно узнать на Mozilla Developer Network.

Повторное объявление и сокрытие переменных

При использовании ключевого слова var неважно сколько раз мы объявим переменную.

function f(x) {
var x;
var x;
    if (true) {
var x;
}
}

В приведенном выше примере все объявления переменной x будут иметь отношение к одному и тому же x, и это вполне допустимо. Как правило, это является источником ошибок. К счастью, с помощью let так делать нельзя.

let x = 10;
let x = 20; // Ошибка: 'x' уже объявлен в этой области видимости

Переменные при этом необязательно всегда должны объявляться с помощью ключевого слова let.

function f(x) {
let x = 100; // Ошибка: переменная совпадает с параметром
}
function g() {
let x = 100;
var x = 100; // Ошибка: нельзя еще раз объявить 'x'
}

Но это не означает, что let-переменную нельзя объявить только один раз в пределах области видимости функции. Переменная, имеющая область видимости на уровне блока, просто должна быть объявлена в другом блоке.

function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
    return x;
}
f(false, 0); // returns '0'
f(true, 0); // returns '100'

Процесс объявления новой переменной во вложенной области видимости называется сокрытием. Это как палка о двух концах: с одной стороны это может привести к ошибкам в случае случайного сокрытия переменной; с другой — предотвратить их появление. Для примера перепишем написанную выше функцию 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;
}

Данная версия функции будет работать корректно, так как переменная i внутреннего цикла скрывает i от внешнего цикла.

Как правило, следует избегать сокрытия для написания более чистого кода. Его нужно использовать только тогда, когда вы уверены, что оно может дать вам преимущество в каждом конкретном случае.

Захват let-переменных

Когда мы первый раз коснулись идеи захвата переменных, объявленных с помощью var, мы кратко описали как происходит этот процесс. Для лучшего понимания рассмотрим этот процесс подробнее. Каждый раз, когда область видимости начинает свое выполнение, она создает “окружение” для переменных, которые в ней используются. Это окружение и захваченные в ней переменные могут существовать даже после того, как выполнение этой области видимости завершится.

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

Так как мы захватили переменную city внутри окружения, созданной областью видимости, мы все еще можем получить к ней доступ несмотря на тот факт, что блок if уже выполнился.

Вспомним пример с функцией setTimeout, описанный ранее. Там нам потребовалось использовать функцию немедленного вызова (IIFE) для захвата значения переменной для каждой итерации цикла. То есть по сути мы создали новые переменные окружения для каждой захваченной переменной. Это головная боль для разработчиков, но к счастью, в TypeScript вам не придется этого делать.

let-объявления переменных обладают кардинально отличающимся поведением, когда переменная объявляется как часть цикла. В этом случае создается область видимости для каждой итерации цикла. Поэтому пример с setTimeout можно переписать следующим образом, используя let:

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

Как и ожидалось, результат будет следующим:

0
1
2
3
4
5
6
7
8
9

Объявления переменных с использованием const

Объявления с помощью ключевого слова const являются еще одним способом создания переменных.

const numLivesForCat = 9;

Объявление с помощью const похоже на let, но, как следует из названия, значение такой переменной может быть присвоено только один раз. Другими словами, к const объявлениям применимы те же правила, что и к let, но плюс к этому вы не можете изменить значение переменной.

Не следует путать это с тем, что значения, на которые ссылаются такие переменные будут неизменяемыми.

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--;

Если не вы не предпримете конкретных действий, чтобы этого избежать, внутреннее состояние константной переменной останется изменяемым. К счастью, в TypeScript можно указать, что члены объекта являются только для чтения. Здесь можно подробнее ознакомиться как это реализовать.

let vs. const

Так когда же использовать let, а когда const? Здесь все зависит от контекста.

Применяя принцип наименьших привилегий, все объявления переменных, которые не планируется изменять, должны быть определены с помощью const. Смысл заключается в том, что если переменную не требуется изменять, другие разработчики не должны автоматически получить доступ к ее изменению. Для того, чтобы это сделать, необходимо будет рассмотреть вопрос о целесообразности данного действия. Использование const делает код более предсказуемым.

Applying the principle of least privilege, all declarations other than those you plan to modify should use const. The rationale is that if a variable didn’t need to get written to, others working on the same codebase shouldn’t automatically be able to write to the object, and will need to consider whether they really need to reassign to the variable. Using constalso makes code more predictable when reasoning about flow of data.

С другой стороны, длина слова let такая же, как и у var, и многие пользователи предпочтут использовать let для краткости.

Выбор за вами.

Деструктуризация

Другая особенность ECMAScript 2015, которую поддерживает TypeScript — деструктуризация. Подробнее с этой особенностью можно ознакомиться в статье.

Разбор массивов

Простейший пример деструктуризации — разбор массива с помощью деструктурирующего присваивания:

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

В данном примере создаются две переменные: first и second. Это более удобно, чем использовать доступ к элементу массива по индексу

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

Деструктуризация работает с уже объявленными переменными:

// меняем значения местами
[first, second] = [second, first];

Также можно использовать данную форму записи в качестве параметра функции:

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

Вы можете создать переменную для оставшихся элементов массива, используя синтаксис вида …name

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

Ну и конечно вы можете игнорировать элементы массива, которые вам не нужны:

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

Или например так:

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

Деструктуризация объектов

Деструктуризацию можно использовать для объектов:

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

В этом примере будут созданы новые переменные a и b, которые будут иметь значения равные o.a и o.b соответственно. Стоит отметить, что свойство c игнорируется за ненадобностью.

Также как и при разборе массивов, вы можете присваивать следующим образом:

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

Обратите внимание на скобки. Без них JavaScript воспримет { как начало блока.

Переименование свойств

Можно давать различные имена свойствам объекта:

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

Как ни странно, но в этой записи newName1 и newName2 не означают тип. Эта запись аналогична:

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

Чтобы указать типы переменных, нужно использовать следующую форму записи:

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

Значения по умолчанию

Значения по умолчанию позволяют задать значение свойству, если оно не определено явно:

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

keepWholeObject теперь имеет переменную wholeObject, также как и свойства a и b, даже если b не определено.

Объявления функций

Деструктуризация также применяется при объявлении функций. Рассмотрим простой случай:

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

Указание значений по умолчанию для параметров функций с использованием деструктуризации может показаться фокусом. В первую очередь нужно запомнить, что тип должен быть указан до значений по умолчанию.

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

Также нужно запомнить, что значения по умолчанию для необязательных свойств должны указываться на деструктурирующем свойстве, а не в инициализаторе.

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

Пользуйтесь деструктуризацией с осторожностью. Как показано выше, даже простейшая деструктуризация имеет определенные особенности. Что уж говорить о глубоко вложенной деструктуризации, которую может оказаться очень непросто понять даже без переименований, значений по умолчанию и определений типов. Старайтесь использовать только простые и понятные формы записи деструктуризации.