Everyday Types
In this chapter, we’ll cover some of the most common types of values you’ll find in JavaScript code, and explain the corresponding ways to describe those types in TypeScript. This isn’t an exhaustive list, and future chapters will describe more ways to name and use other types.
Types can also appear in many more places than just type annotations. As we learn about the types themselves, we’ll also learn about the places where we can refer to these types to form new constructs.
We’ll start by reviewing the most basic and common types you might encounter when writing JavaScript or TypeScript code. These will later form the core «building blocks» of more complex types.
Primitives string , number , and boolean
JavaScript has three main primitive kinds of values: string , number , and boolean . Each has a corresponding type in TypeScript. As you might expect, these are the same names you’d see if you used the JavaScript typeof operator on a value of those types:
- string represents string values like «Hello, world»
- number is for numbers like 42 . JavaScript does not have a special runtime value for integers, so there’s no equivalent to int or float — everything is simply number
- boolean is for the two values true and false
The type names String , Number , and Boolean (starting with capital letters) are legal, but refer to some special built-in types that shouldn’t appear in your code. Always use string , number , or boolean .
Arrays
To specify the type of an array like [1, 2, 3] , you can use the syntax number[] ; this syntax works for any type (e.g. string[] is an array of strings, and so on). You may also see this written as Array<number> , which means the same thing. We’ll learn more about the syntax T<U> when we cover generics.
Note that [number] is a different thing; refer to the section on tuple types.
TypeScript also has a special type, any , that you can use whenever you don’t want a particular value to cause typechecking errors.
When a value is of type any , you can access any properties of it (which will in turn be of type any ), call it like a function, assign it to (or from) a value of any type, or pretty much anything else that’s syntactically legal:
The any type is useful when you don’t want to write out a long type just to convince TypeScript that a particular line of code is okay.
noImplicitAny
When a type isn’t specified and can’t be inferred from context, TypeScript will typically default to any . Because any values don’t benefit from type-checking, it’s usually desirable to avoid these situations. The compiler flag noImplicitAny will cause any implicit any to be flagged as an error.
Type Annotations on Variables
When you declare a variable using const , var , or let , you can optionally add a type annotation to explicitly specify the type of the variable:
TypeScript doesn’t use «types on the left»-style declarations like int x = 0; Type annotations will always go after the thing being typed.
In most cases, though, this isn’t needed. Wherever possible, TypeScript tries to automatically infer the types in your code. For example, the type of a variable is inferred based on the type of its initializer:
For the most part you don’t need to explicitly learn the rules of inference. If you’re starting out, try using fewer type annotations than you think — you might be surprised how few you need for TypeScript to fully understand what’s going on.
Functions
Functions are the primary means of passing data around in JavaScript. TypeScript allows you to specify the types of both the input and output values of functions.
Parameter Type Annotations
When you declare a function, you can add type annotations after each parameter to declare what kinds of parameters the function accepts. Parameter type annotations go after the parameter name:
When a parameter has a type annotation, calls to that function will be validated:
Return Type Annotations
You can also add return type annotations. Return type annotations appear after the parameter list:
Much like variable type annotations, you usually don’t need a return type annotation because TypeScript will infer the function’s return type based on its return statements. The type annotation in the above example doesn’t change anything. Some codebases will explicitly specify a return type for documentation purposes, to prevent accidental changes, or just for personal preference.
Function Expressions
Function expressions are a little bit different from function declarations. When a function expression appears in a place where TypeScript can determine how it’s going to be called, the parameters of that function are automatically given types.
Here’s an example:
Even though the parameter s didn’t have a type annotation, TypeScript used the types of the forEach function, along with the inferred type of the array, to determine the type s will have.
This process is called contextual typing because the context that the function occurred in informed what type it should have. Similar to the inference rules, you don’t need to explicitly learn how this happens, but understanding that it does happen can help you notice when type annotations aren’t needed. Later, we’ll see more examples of how the context that a value occurs in can affect its type.
Object Types
Apart from primitives, the most common sort of type you’ll encounter is an object type. This refers to any JavaScript value with properties, which is almost all of them! To define an object type, we simply list its properties and their types.
For example, here’s a function that takes a point-like object:
Here, we annotated the parameter with a type with two properties — x and y — which are both of type number . You can use , or ; to separate the properties, and the last separator is optional either way.
The type part of each property is also optional. If you don’t specify a type, it will be assumed to be any .
Optional Properties
Object types can also specify that some or all of their properties are optional. To do this, add a ? after the property name:
In JavaScript, if you access a property that doesn’t exist, you’ll get the value undefined rather than a runtime error. Because of this, when you read from an optional property, you’ll have to check for undefined before using it.
Union Types
TypeScript’s type system allows you to build new types out of existing ones using a large variety of operators. Now that we know how to write a few types, it’s time to start combining them in interesting ways.
Defining a Union Type
The first way to combine types you might see is a union type. A union type is type formed from two or more other types, representing values that may be any one of those types. We refer to each of these types as the union’s members.
Let’s write a function that can operate on strings or numbers:
Working with Union Types
It’s easy to provide a value matching a union type — simply provide a type matching any of the union’s members. If you have a value of a union type, how do you work with it?
TypeScript will only allow you to do things with the union if that thing is valid for every member of the union. For example, if you have the union string | number , you can’t use methods that are only available on string :
The solution is to narrow the union with code, the same as you would in JavaScript without type annotations. Narrowing occurs when TypeScript can deduce a more specific type for a value based on the structure of the code.
For example, TypeScript knows that only a string value will have a typeof value «string» :
Another example is to use a function like Array.isArray :
Notice that in the else branch, we don’t need to do anything special — if x wasn’t a string[] , then it must have been a string .
Sometimes you’ll have a union where all the members have something in common. For example, both arrays and strings have a slice method. If every member in a union has a property in common, you can use that property without narrowing:
It might be confusing that a union of types appears to have the intersection of those types’ properties. This is not an accident — the name union comes from type theory. The union number | string is composed by taking the union of the values from each type. Notice that given two sets with corresponding facts about each set, only the intersection of those facts applies to the union of the sets themselves. For example, if we had a room of tall people wearing hats, and another room of Spanish speakers wearings hats, after combining those rooms, the only thing we know about every person is that they must be wearing a hat.
Type Aliases
We’ve been using object types and union types by writing them directly in type annotations. This is convenient, but it’s common to want to use the same type more than once and refer to it by a single name.
A type alias is exactly that — a name for any type. The syntax for a type alias is:
You can actually use a type alias to give a name to any type at all, not just an object type. For example, a type alias can name a union type:
Note that aliases are only aliases — you cannot use type aliases to create different/distinct «versions» of the same type. When you use the alias, it’s exactly as if you had written the aliased type. In other words, this code might look illegal, but is OK according to TypeScript because both types are aliases for the same type:
Interfaces
An interface declaration is another way to name an object type:
Just like when we used a type alias above, the example works just as if we had used an anonymous object type. TypeScript is only concerned with the structure of the value we passed to printCoord — it only cares that it has the expected properties. Being concerned only with the structure and capabilities of types is why we call TypeScript a structurally typed type system.
Differences Between Type Aliases and Interfaces
Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Here are the most relevant differences between the two that you should be aware of. You’ll learn more about these concepts in later chapters, so don’t worry if you don’t understand all of these right away.
- Interfaces may be extend ed, but not type aliases. We’ll discuss this later, but it means that interfaces can provide more guarantees when creating new types out of other types.
- Type aliases may not participate in declaration merging, but interfaces can.
- Interfaces may only be used to declare object types.
- Interface names will always appear in their original form in error messages, but only when they are used by name.
- Type alias names may appear in error messages, sometimes in place of the equivalent anonymous type (which may or may not be desirable).
For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs something to be the other kind of declaration.
Type Assertions
Sometimes you will have information about the type of a value that TypeScript can’t know about.
For example, if you’re using document.getElementById , TypeScript only knows that this will return some kind of HTMLElement , but you might know that your page will always have an HTMLCanvasElement with a given ID.
In this situation, you can use a type assertion to specify a more specific type:
Like a type annotation, type assertions are removed by the compiler and won’t affect the runtime behavior of your code.
You can also use the angle-bracket syntax (except if the code is in a .tsx file), which is equivalent:
Reminder: Because they are removed at compile-time, there is no runtime checking associated with a type assertion. There won’t be an exception or null generated if the type assertion is wrong.
TypeScript only allows type assertions which convert to a more specific or less specific version of a type. This rule prevents «impossible» coercions like:
Sometimes this rule can be too conservative and will disallow more complex coercions that might be valid. If this happens, you can use two assertions, first to any (or unknown , which we’ll introduce later), then to the desired type:
Literal Types
In addition to the general types string and number , we can refer to specific strings and numbers in type positions.
By themselves, literal types aren’t very valuable:
It’s not much use to have a variable that can only have one value!
But by combining literals into unions, you can express a much more useful thing — for example, functions that only accept a certain set of known values:
Numeric literal types work the same way:
Of course, you can combine these with non-literal types:
There’s one more kind of literal type: boolean literals. There are only two boolean literal types, and as you might guess, they are the types true and false . The type boolean itself is actually just an alias for the union true | false .
Literal Inference
When you initialize a variable with an object, TypeScript assumes that the properties of that object might change values later. For example, if you wrote code like this:
TypeScript doesn’t assume the assignment of 1 to a field that previously had 0 to be an error. Another way of saying this is that obj.counter must have the type number , not 0 , because types are used to determine both reading and writing behavior.
The same applies to strings:
Because it’d be legal to assign a string like «GUESS» TO req.method , TypeScript considers this code to have an error. You can change this inference by adding a type assertion in either location:
The first change means «I intend for req.method to always have the literal type «GET» «, preventing the possible assignment of «GUESS» to that field. The second change means «I know for other reasons that req.method has the value «GET» «.
null and undefined
JavaScript has two primitive values, null and undefined , both of which are used to signal absent or uninitialized values.
TypeScript has two corresponding types by the same names. How these types behave depends on whether you have the strictNullChecks option on.
strictNullChecks off
With strictNullChecks off, values that might be null or undefined can still be accessed normally, and the values null and undefined can be assigned to a property of any type. This is similar to how languages without null checks (e.g. C#, Java) behave. The lack of checking for these values tends to be a major source of bugs; we always recommend people turn strictNullChecks on if it’s practical to do so in their codebase.
strictNullChecks on
With strictNullChecks on, when a value is null or undefined , you will need to test for those values before using methods or properties on that value. Just like checking for undefined before using an optional property, we can use narrowing to check for values that might be null :
Non-null Assertion Operator (Postfix ! )
TypeScript also has a special syntax for removing null and undefined from a type without doing any explicit checking. Writing ! after any expression is effectively a type assertion that the value isn’t null or undefined :
Just like other type assertions, this doesn’t change the runtime behavior of your code, so it’s important to only use ! when you know that the value can’t be null or undefined .
Variable Declarations #
let и const — относительно новые типы объявления переменных в JavaScript. Как мы упомянули ранее, let похож на var в некотором смысле, но позволяет пользователям избежать некоторые из общих ошибок, с которыми сталкиваются в JavaScript. const это расширение let , которое предотвращает переопределение переменных.
Так как TypeScript является надстройкой над Javascript, язык также поддерживает let и const . Далее мы подробнее расскажем об этих новых объявлениях переменных и объясним, почему они более предпочтительны, чем var .
Если вы использовали JavaScript поверхностно, следующая секция секция поможет освежить некоторые важные моменты. Если вы хорошо знакомы со всеми причудами объявления var в JavaScript, вы можете пропустить эту часть.
Объявления var #
Объявление переменной в JavaScript всегда происходит с помощью ключевого слова var .
Как вы наверняка поняли, мы только что объявили переменную с именем a и значением 10 .
Мы также можем объявить переменную внутри функции:
и мы также имеем доступ к этим переменным внутри других функций:
В примере выше g захватывает(замыкает в себе) переменную a , объявленную в f . В любой точке, где будет вызвана g , значение a будет связано со значением a в функции f . Даже если g вызвана однажды и f закончила выполнение, можно получить доступ и модифицировать a .
Правила области видимости (Scoping)
Объявление var имеет несколько странных правил области видимости для тех, кто использует другие языки программирования. Посмотрите не следующий пример:
Некоторые могут повторно посмотреть на тот пример. Переменная x была объявлена внутри блока if , и мы можем получить к ней доступ вне этого блока. Это потому что объявления var доступны где бы то ни было внутри содержащей их функции, модуля, пространства имен(namespace) или же глобальной области видимости несмотря на блок, в котором они содержатся. Некоторые называют это var -видимость or function-видимость. Параметры также видны внутри функции.
Эти правила области видимости могут вызвать несколько типов ошибок. Одна из раздражающих проблем — это то, что не является ошибкой объявление переменной несколько раз:
Скорее всего несложно заметить, что внутренний цикл for случайно перезапишет переменную i , потому что i имеет области видимости внутри функции sumMatrix. Опытные разработчики знают, что похожие ошибки проскальзывают при code review и могут быть причиной бесконечной фрустрации.
Variable capturing quirks
Попробуйте быстро догадаться, какой будет вывод у этого кода:
Для тех, кто незнаком, setTimeout пытается выполнить функцию после указанного количества миллисекунд (при это ожидая, пока какой-либо другой код прекратит выполняться)
Готовы? Вот результат::
Многие JavaScript разработчики знакомы с таким поведением, но если вы удивлены, вы определенно не одиноки. Большинство ожидает, что вывод будет таким:
Помните, что мы упомянули ранее о замыкании переменных?
В любой точке, где будет вызвана g , значение a будет связано со значением a в функции f .
Давайте рассмотрим это в контексте нашего примера. setTimeout запустит функцию через несколько миллисекунд, после завершения цикла for . К моменту, когда цикл for закончит выполнение, i будет равняться 10 . Поэтому каждый раз, когда отложенная функция будет вызвана, она возвратит 10 !
Самый простой способ решить проблему — использовать немедленный запуск анонимной функции, чтобы захватить i на каждой итерации:
Этот странно выглядящий шаблон на самом деле не редок.
Объявления let #
Сейчас мы уже понимаем, что var имеет некоторые проблемы, именно поэтому появился новый способ объявления переменных let . Они записываются точно также, как и объявления var .
Ключевое различие не в синтаксисе, а в семантике, в которую мы сейчас погрузимся.
Блочная область видимости
Когда переменная объявляется с использованием let , она используется в режиме блочной области видимости. В отличие от переменных, объявленных с помощью var , чьи области видимости распространяются на всю функцию, в которой они находятся, переменные блочной области видимости не видимы вне их ближайшего блока или же цикла for .
Здесь мы имеем две локальные переменные a и b . Область видимости a ограничена телом функции f , в то время как область b ограничена блоком условия if .
Переменные, объявленные в блоке catch имеют те же правила видимости.
Другое свойство переменных блочной области видимости — к ним нельзя обратиться перед тем, как они были объявлены. При том, что переменные блочной области видимости представлены везде в своем блоке, в каждой точке до их объявления находится мертвая зона. Это просто такой способ сказать, что вы не можете получить к ним доступ до утверждения let и, к счастью, TypeScript напомнит вам об этом.
Однако, вы все еще можете захватить (замкнуть) переменную с блочной областью видимости до ее объявления. Правда, попытка вызвать такую функцию до ее объявления приведет к ошибке. Если вы компилируете в стандарт ES2015, это вызовет ошибку; тем не менее, прямо сейчас TypeScript разрешает это и не будет указывать на ошибку.
Для более подробной информации о мертвых зонах перейдите по этой ссылке: Mozilla Developer Network.
Повторное объявление и экранирование
В случае объявлений var не имеет значения, как много раз вы объявляете одну и ту же переменную. Вы всегда получите одну.
В примере выше все объявления x на самом деле указывают на одну и ту же x , и это вполне допустимо. Это часто является источником багов. Поэтому хорошо, что объявления let этого не позволяют.
Переменные не обязательно должны обе быть с блочной областью видимости в TypeScript, чтобы компилятором была указана ошибка.
Это не значит, что переменная с блочной областью видимости не может быть объявлена с переменной с областью видимости в той же функции. Переменная с блочной областью просто должна быть объявлена в своем блоке
Способ введения нового имени во вложенной области называется сокрытием. Это своего рода меч с двумя лезвиями, т.к. он может ввести некоторые баги, также как и избавить от других. Например, представьте, как мы могли бы переписать функцию sumMatrix , используя переменные let .
Эта версия цикла делает суммирование корректно, потому что i внутреннего цикла перекрывает i внешнего.
Такое сокрытие нужно обычно избегать, чтобы код был чище. Но в некоторых сценариях такой способ может идеально подходить для решения задачи. Вы должны использовать лучшее решение на ваше усмотрение.
Замыкание пременных с блочной областью видмости
Когда мы впервые коснулись замыкания переменных с объявлением var , мы коротко рассмотрели, как переменные ведут себя при замыкании. Чтобы лучше понимать суть, представьте себе, что каждый раз, когда появляется новая область видимости, она создает свою «среду» для переменных. Эта среда и ее захваченные извне переменные могут существовать даже после того, как все выражения внутри области видимости завершили свое выполнение.
Из-за того, что мы захватили переменную city из ее среды, мы все еще можем получить к ней доступ, несмотря на тот факт, что блок if закончил выполнение. Вспомните наш предыдущий пример с setTimeout . Мы закончили на необходимости использовать IIFE, чтобы захватить состояние переменной для кждой итерации цикла for . В результате мы каждый раз создавали новую среду переменных для наших захваченных. Это доставляло немного боли, но, к счастью, нам не потребуется делать это снова в TypeScript.
Объявления let ведут себя совсем иначе, когда являются частью цикла. Вместо того, чтобы вводить новую среду для цикла, они вводят новую область видимости для каждой итерации. Так как это то, что мы делали с нашим IIFE, мы можем изменить наш старый пример setTimeout , используя объявления let .
and as expected, this will print out
Объявления const #
Объявления const — это еще один способ объявления переменных.
Они такие же как и let , только, согласно их названию, их значение не может быть изменено после того, как им однажды уже присвоили значение. Другими словами, к ним применимы все правила области видимости let , но вы не можете их переназначить. Значение, с которым они связаны, является неизменным.
Несмотря на то, что переменная была объявлена как const , ее внутренее состояние все еще может быть изменено. К счастью, TypeScript позволяет вам определить свойства объекта доступными только на чтение: readonly . Раздел в Интерфейсах объясняет детали этого.
let или const ? #
У нас есть два способа объявления с похожими правилами их области видимости, поэтому сам собой напрашивается вопрос о том, какой использовать. Ответ будет таким же, как и на большинство широких вопросов: это зависит от обстоятельств.
Применяя принцип наименьшего уровня привелегий, все объявления переменных, которые вы в дальнейшем не планируете менять, должны использовать const . Объясняется это тем, что если переменная не должна изменять свое значение, другие разработчики, которые работают над тем же кодом, не должны иметь возможность записи в объект. Это должно быть позволено только в случае реальной необходимости переназначения переменной. Использование const делает код более предсказуемым и понятным при объяснении потока данных.
Большая часть этого руководства использует объявления let .
Деструктурирование #
Еще одно нововведение из стандарта ECMAScript 2015, которое есть в TypeScript, это деструктурирование (прим. переводчика «destructuring» — не «уничтожение»). За полной информацией об этом пройдите по ссылке статья на Mozilla Developer Network. В этом разделе мы приведем сокращенный вариант.
Деструктурирование массивов
Самая простая форма деструктурирования — с использованием массива:
Это создает две новых переменных с именами first и second . В сущности это эквивалент обращения по индексу, просто более удобный:
Деструктурирование также работает с ранее объявленными переменными:
И с параметрами функции:
Вы можете создать переменную для оставшихся элементов списка, используя ситаксис . name :
Конечно, т.к. это Javascript, вы можете просто проигнорировать их:
Или какие-то определенные элементы:
Деструктурирование объекта
Вы можете также деструктурировать объекты:
Этот код создает новые переменные a и b из o.a и o.b . Заметьте, что вы можете пропустить c , если она вам не нужна.
Как и в случае деструктурирования массива, вы можете назначить значения без объявления:
Заметьте, что мы должны окружить это выражение с помощью круглых скобок. JavaScript при парсинге рассматривает < как старт нового блока.
Переименование свойств
Вы можете также дать разные имена свойствам:
Этот синтакс может немного сущать. Вы читаете a: newName1 как » a as newName1 «. Можно записать это по-другому, чтобы было понятнее:
Сбивает с толку то, что двоеточие здесь не обозначает тип. Тип, если вы задаете его, все еще должен быть записан после деструктурирования:
Значения по умолчанию
Значения по умолчанию позволяют определить свойство, даже если оно не задавалось:
Функция keepWholeObject имеет переменную для wholeObject , как и свойства a and b , даже если b не определено.
Объявление функций
Деструктурирование также работает с объявлением функций Ниже приведен простой пример:
Указание значений по умолчанию чаще употребляется для параметров, и использвание деструктурирования для этого может выглядеть запутанно. Прежде всего, вы должны помнить о необходимости указать тип до значения по умолчанию.
Затем, вы должны не забыть дать значение по умолчанию для опциональных свойств деструктурированного параметра при определении функции. Помните также, что C был определен с опциональным свойством b :
Используйте деструктурирование с осторожностью. Как показал предыдущий пример, все сложные выражения деструктурирования имеют много ньюансов. В особенности это касается многоуровневого вложенного деструктурирования, которое действительно сложно для понимания даже без переименования, значений по умолчанию и аннотаций типа. Старайтесь оставлять выражения деструктурирования маленькими и простыми.
Поддержите перевод документации:
Поддерживатель | Github Репозиторий
Documentation generated by mdoc.
Объявление переменных в TypeScript
![]()
В JavaScript существует два относительно новых способа объявления переменных — с помощью ключевых слов let и const. Объявление переменной с использованием let очень похоже на объявление с использованием var, но позволяет избегать некоторых распространенных подводных камней при работе с JavaScript. Объявление с помощью const делает невозможным все последующие присваивания новых значений переменной.
Язык TypeScript полностью поддерживает объявление переменных с помощью let и const, поскольку является надмножеством языка JavaScript. Далее будет рассмотрено, почему let и const являются более предпочтительным способом объявления переменных, нежели var.
Объявление с помощью var
Использование ключевого слова var для объявления переменной является традиционным способом в языке JavaScript.
Как можно видеть, здесь мы объявили переменную с именем a и присвоили ей значение, равное 10.
Также объявлять переменную можно внутри функции:
Ну и конечно можно получить доступ к этим переменным внутри других функций:
В приведенном выше примере, функция g захватывает переменную а, объявленную в функции f. Когда будет вызвана функция g, значение переменной a в g будет привязано к значению a из f. Таким образом даже при однократном вызове функции g в f, g получает доступ к переменной a и может изменить её значение.
Правила области видимости
Объявления переменных с использованием var имеют несколько необычные правила области видимости по сравнению с другими языками программирования. Рассмотрим пример:
В данном примере переменная x объявляется внутри блока if, однако мы можем получить к ней доступ вне его. Происходит это потому, что объявления с помощью var доступны в любом месте в пределах Lсодержащей их функции, модуля, пространства имен, или в глобальном масштабе независимо от содержащего блока. Иногда это называют var-областью видимости (var-scoping) или областью видимости функции. Параметры, передаваемые в функцию, также принадлежат области видимости функции.
Данные правила могут быть причиной нескольких типов ошибок. Одна из проблем — это отсутствие сообщений об ошибках при многократном объявлении одной и той же переменной:
В данном примере объявление переменной во внутреннем цикле for затрет значение переменной i во внешнем цикле, поскольку i находится в пределах одной и той же области видимости функции. Подобные случаи могут ускользнуть от внимания разработчиков и, в конечном итоге, привести к трудно отлавливаемым ошибкам.
Особенности захвата переменных
Рассмотрим следующий пример. Каким будет результат работы данного куска кода?
Если вы еще не знаете, то setTimeout выполняет функцию, переданную в качестве первого аргумента спустя некоторое количество миллисекунд, переданное во втором аргументе.
Так каким же будет результат?
Для множества JavaScript разработчиков такое поведение будет вполне ожидаемым, однако если для вас это сюрприз, то вы в любом случае не одни. Казалось бы, стоило ожидать следующего результата:
Что же здесь не так? На самом деле каждая функция, которую мы передаем в setTimeout ссылается на одну и ту же переменную i из одной и той же области видимости.
setTimeout выполнит функцию спустя некоторое количество миллисекунд, но это будет уже после того, как цикл завершится. И в этом случае значение переменной i будет равно 10.
Выходом из этой ситуации является использование “функции немедленного вызова” (IIFE — an Immediately Invoked Function Expression) для захвата переменной i в каждой итерации цикла:
Такой странный шаблон достаточно распространен в JavaScript. Параметр i “закрывает” переменную i, объявленную в цикле, а поскольку имена переменной и параметра функции совпадают, мы больше не можем изменить переменную цикла.
Объявления с помощью let
Именно из-за проблем, присущим объявлению переменных с использованием var, было введено объявление с использованием let.
Ключевым отличием здесь является не синтаксис, а семантика объявления, которое сейчас и будет рассмотрено.
Область видимости на уровне блока
Когда переменная объявляется с помощью ключевого слова let, считается, что она находится в так называемой лексической области видимости или в области видимости блока. В отличие от var, let переменные не видны вне блока, в котором они были объявлены.
В данном примере переменная а принадлежит области видимости функции f, а область видимости переменной b ограничена блоком if. Поэтому попытка получить доступ к переменной b вне блока if приведет к ошибке.
Переменные, определенные в блоке catch, не доступны за пределами этого блока, что похоже на объявление переменных с помощью let.
К переменным, определенным с помощью ключевого слова let, нельзя обращаться до места их непосредственного объявления. Хотя переменная и существует в определенной области видимости, все попытки обращения к ней до объявления находятся в так называемой “временной мертвой зоне”.
Стоит отметить, что сейчас можно захватить let-переменную до ее объявления. Единственная проблема в том, что нельзя вызывать функцию до объявления этой переменной. Если используется ES2015, то будет сгенерировано исключение; однако, на данный момент TypeScript не выдаст никакого сообщения об ошибке.
Более подробно о “временных мертвых зонах” можно узнать на Mozilla Developer Network.
Повторное объявление и сокрытие переменных
При использовании ключевого слова var неважно сколько раз мы объявим переменную.
В приведенном выше примере все объявления переменной x будут иметь отношение к одному и тому же x, и это вполне допустимо. Как правило, это является источником ошибок. К счастью, с помощью let так делать нельзя.
Переменные при этом необязательно всегда должны объявляться с помощью ключевого слова let.
Но это не означает, что let-переменную нельзя объявить только один раз в пределах области видимости функции. Переменная, имеющая область видимости на уровне блока, просто должна быть объявлена в другом блоке.
Процесс объявления новой переменной во вложенной области видимости называется сокрытием. Это как палка о двух концах: с одной стороны это может привести к ошибкам в случае случайного сокрытия переменной; с другой — предотвратить их появление. Для примера перепишем написанную выше функцию sumMatrix с использованием let.
Данная версия функции будет работать корректно, так как переменная i внутреннего цикла скрывает i от внешнего цикла.
Как правило, следует избегать сокрытия для написания более чистого кода. Его нужно использовать только тогда, когда вы уверены, что оно может дать вам преимущество в каждом конкретном случае.
Захват let-переменных
Когда мы первый раз коснулись идеи захвата переменных, объявленных с помощью var, мы кратко описали как происходит этот процесс. Для лучшего понимания рассмотрим этот процесс подробнее. Каждый раз, когда область видимости начинает свое выполнение, она создает “окружение” для переменных, которые в ней используются. Это окружение и захваченные в ней переменные могут существовать даже после того, как выполнение этой области видимости завершится.
Так как мы захватили переменную city внутри окружения, созданной областью видимости, мы все еще можем получить к ней доступ несмотря на тот факт, что блок if уже выполнился.
Вспомним пример с функцией setTimeout, описанный ранее. Там нам потребовалось использовать функцию немедленного вызова (IIFE) для захвата значения переменной для каждой итерации цикла. То есть по сути мы создали новые переменные окружения для каждой захваченной переменной. Это головная боль для разработчиков, но к счастью, в TypeScript вам не придется этого делать.
let-объявления переменных обладают кардинально отличающимся поведением, когда переменная объявляется как часть цикла. В этом случае создается область видимости для каждой итерации цикла. Поэтому пример с setTimeout можно переписать следующим образом, используя let:
Как и ожидалось, результат будет следующим:
Объявления переменных с использованием const
Объявления с помощью ключевого слова const являются еще одним способом создания переменных.
Объявление с помощью const похоже на let, но, как следует из названия, значение такой переменной может быть присвоено только один раз. Другими словами, к const объявлениям применимы те же правила, что и к let, но плюс к этому вы не можете изменить значение переменной.
Не следует путать это с тем, что значения, на которые ссылаются такие переменные будут неизменяемыми.
Если не вы не предпримете конкретных действий, чтобы этого избежать, внутреннее состояние константной переменной останется изменяемым. К счастью, в 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 const also makes code more predictable when reasoning about flow of data.
С другой стороны, длина слова let такая же, как и у var, и многие пользователи предпочтут использовать let для краткости.
Деструктуризация
Другая особенность ECMAScript 2015, которую поддерживает TypeScript — деструктуризация. Подробнее с этой особенностью можно ознакомиться в статье.
Разбор массивов
Простейший пример деструктуризации — разбор массива с помощью деструктурирующего присваивания:
В данном примере создаются две переменные: first и second. Это более удобно, чем использовать доступ к элементу массива по индексу
Деструктуризация работает с уже объявленными переменными:
Также можно использовать данную форму записи в качестве параметра функции:
Вы можете создать переменную для оставшихся элементов массива, используя синтаксис вида …name
Ну и конечно вы можете игнорировать элементы массива, которые вам не нужны:
Или например так:
Деструктуризация объектов
Деструктуризацию можно использовать для объектов:
В этом примере будут созданы новые переменные a и b, которые будут иметь значения равные o.a и o.b соответственно. Стоит отметить, что свойство c игнорируется за ненадобностью.
Также как и при разборе массивов, вы можете присваивать следующим образом:
Обратите внимание на скобки. Без них JavaScript воспримет < как начало блока.
Переименование свойств
Можно давать различные имена свойствам объекта:
Как ни странно, но в этой записи newName1 и newName2 не означают тип. Эта запись аналогична:
Чтобы указать типы переменных, нужно использовать следующую форму записи:
Значения по умолчанию
Значения по умолчанию позволяют задать значение свойству, если оно не определено явно:
keepWholeObject теперь имеет переменную wholeObject, также как и свойства a и b, даже если b не определено.
Объявления функций
Деструктуризация также применяется при объявлении функций. Рассмотрим простой случай:
Указание значений по умолчанию для параметров функций с использованием деструктуризации может показаться фокусом. В первую очередь нужно запомнить, что тип должен быть указан до значений по умолчанию.
Также нужно запомнить, что значения по умолчанию для необязательных свойств должны указываться на деструктурирующем свойстве, а не в инициализаторе.
Пользуйтесь деструктуризацией с осторожностью. Как показано выше, даже простейшая деструктуризация имеет определенные особенности. Что уж говорить о глубоко вложенной деструктуризации, которую может оказаться очень непросто понять даже без переименований, значений по умолчанию и определений типов. Старайтесь использовать только простые и понятные формы записи деструктуризации.
Variable Declaration
let and const are two relatively new concepts for variable declarations in JavaScript. As we mentioned earlier, let is similar to var in some respects, but allows users to avoid some of the common “gotchas” that users run into in JavaScript.
const is an augmentation of let in that it prevents re-assignment to a variable.
With TypeScript being an extension of JavaScript, the language naturally supports let and const . Here we’ll elaborate more on these new declarations and why they’re preferable to var .
If you’ve used JavaScript offhandedly, the next section might be a good way to refresh your memory. If you’re intimately familiar with all the quirks of var declarations in JavaScript, you might find it easier to skip ahead.
Declaring a variable in JavaScript has always traditionally been done with the var keyword.
As you might’ve figured out, we just declared a variable named a with the value 10 .
We can also declare a variable inside of a function:
and we can also access those same variables within other functions:
In this above example, g captured the variable a declared in f . At any point that g gets called, the value of a will be tied to the value of a in f . Even if g is called once f is done running, it will be able to access and modify a .
var declarations have some odd scoping rules for those used to other languages. Take the following example:
Some readers might do a double-take at this example. The variable x was declared within the if block, and yet we were able to access it from outside that block. That’s because var declarations are accessible anywhere within their containing function, module, namespace, or global scope — all which we’ll go over later on — regardless of the containing block. Some people call this var -scoping or function-scoping. Parameters are also function scoped.
These scoping rules can cause several types of mistakes. One problem they exacerbate is the fact that it is not an error to declare the same variable multiple times:
Maybe it was easy to spot out for some experienced JavaScript developers, but the inner for -loop will accidentally overwrite the variable i because i refers to the same function-scoped variable. As experienced developers know by now, similar sorts of bugs slip through code reviews and can be an endless source of frustration.
Variable capturing quirks
Take a quick second to guess what the output of the following snippet is:
For those unfamiliar, setTimeout will try to execute a function after a certain number of milliseconds (though waiting for anything else to stop running).
Ready? Take a look:
Many JavaScript developers are intimately familiar with this behavior, but if you’re surprised, you’re certainly not alone. Most people expect the output to be
Remember what we mentioned earlier about variable capturing? Every function expression we pass to setTimeout actually refers to the same i from the same scope.
Let’s take a minute to consider what that means. setTimeout will run a function after some number of milliseconds, but only after the for loop has stopped executing; By the time the for loop has stopped executing, the value of i is 10 . So each time the given function gets called, it will print out 10 !
A common work around is to use an IIFE — an Immediately Invoked Function Expression — to capture i at each iteration:
This odd-looking pattern is actually pretty common. The i in the parameter list actually shadows the i declared in the for loop, but since we named them the same, we didn’t have to modify the loop body too much.
By now you’ve figured out that var has some problems, which is precisely why let statements were introduced. Apart from the keyword used, let statements are written the same way var statements are.
The key difference is not in the syntax, but in the semantics, which we’ll now dive into.
When a variable is declared using let , it uses what some call lexical-scoping or block-scoping. Unlike variables declared with var whose scopes leak out to their containing function, block-scoped variables are not visible outside of their nearest containing block or for -loop.
Here, we have two local variables a and b . a ’s scope is limited to the body of f while b ’s scope is limited to the containing if statement’s block.
Variables declared in a catch clause also have similar scoping rules.
Another property of block-scoped variables is that they can’t be read or written to before they’re actually declared. While these variables are “present” throughout their scope, all points up until their declaration are part of their temporal dead zone. This is just a sophisticated way of saying you can’t access them before the let statement, and luckily TypeScript will let you know that.
Something to note is that you can still capture a block-scoped variable before it’s declared. The only catch is that it’s illegal to call that function before the declaration. If targeting ES2015, a modern runtime will throw an error; however, right now TypeScript is permissive and won’t report this as an error.
For more information on temporal dead zones, see relevant content on the Mozilla Developer Network.
Re-declarations and Shadowing
With var declarations, we mentioned that it didn’t matter how many times you declared your variables; you just got one.
In the above example, all declarations of x actually refer to the same x , and this is perfectly valid. This often ends up being a source of bugs. Thankfully, let declarations are not as forgiving.
The variables don’t necessarily need to both be block-scoped for TypeScript to tell us that there’s a problem.
That’s not to say that a block-scoped variable can never be declared with a function-scoped variable. The block-scoped variable just needs to be declared within a distinctly different block.
The act of introducing a new name in a more nested scope is called shadowing. It is a bit of a double-edged sword in that it can introduce certain bugs on its own in the event of accidental shadowing, while also preventing certain bugs. For instance, imagine we had written our earlier sumMatrix function using let variables.
This version of the loop will actually perform the summation correctly because the inner loop’s i shadows i from the outer loop.
Shadowing should usually be avoided in the interest of writing clearer code. While there are some scenarios where it may be fitting to take advantage of it, you should use your best judgement.
Block-scoped variable capturing
When we first touched on the idea of variable capturing with var declaration, we briefly went into how variables act once captured. To give a better intuition of this, each time a scope is run, it creates an “environment” of variables. That environment and its captured variables can exist even after everything within its scope has finished executing.
Because we’ve captured city from within its environment, we’re still able to access it despite the fact that the if block finished executing.
Recall that with our earlier setTimeout example, we ended up needing to use an IIFE to capture the state of a variable for every iteration of the for loop. In effect, what we were doing was creating a new variable environment for our captured variables. That was a bit of a pain, but luckily, you’ll never have to do that again in TypeScript.
let declarations have drastically different behavior when declared as part of a loop. Rather than just introducing a new environment to the loop itself, these declarations sort of create a new scope per iteration. Since this is what we were doing anyway with our IIFE, we can change our old setTimeout example to just use a let declaration.
and as expected, this will print out
const declarations are another way of declaring variables.
They are like let declarations but, as their name implies, their value cannot be changed once they are bound. In other words, they have the same scoping rules as let , but you can’t re-assign to them.
This should not be confused with the idea that the values they refer to are immutable.
Unless you take specific measures to avoid it, the internal state of a const variable is still modifiable. Fortunately, TypeScript allows you to specify that members of an object are readonly . The chapter on Interfaces has the details.
Given that we have two types of declarations with similar scoping semantics, it’s natural to find ourselves asking which one to use. Like most broad questions, the answer is: it depends.
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 const also makes code more predictable when reasoning about flow of data.
Use your best judgement, and if applicable, consult the matter with the rest of your team.
The majority of this handbook uses let declarations.
Another ECMAScript 2015 feature that TypeScript has is destructuring. For a complete reference, see the article on the Mozilla Developer Network. In this section, we’ll give a short overview.
The simplest form of destructuring is array destructuring assignment:
This creates two new variables named first and second . This is equivalent to using indexing, but is much more convenient:
Destructuring works with already-declared variables as well:
And with parameters to a function:
You can create a variable for the remaining items in a list using the syntax . :
Of course, since this is JavaScript, you can just ignore trailing elements you don’t care about:
Or other elements:
Tuples may be destructured like arrays; the destructuring variables get the types of the corresponding tuple elements:
It’s an error to destructure a tuple beyond the range of its elements:
As with arrays, you can destructure the rest of the tuple with . , to get a shorter tuple:
Or ignore trailing elements, or other elements:
You can also destructure objects:
This creates new variables a and b from o.a and o.b . Notice that you can skip c if you don’t need it.
Like array destructuring, you can have assignment without declaration:
Notice that we had to surround this statement with parentheses. JavaScript normally parses a < as the start of block.
You can create a variable for the remaining items in an object using the syntax . :
You can also give different names to properties:
Here the syntax starts to get confusing. You can read a: newName1 as ” a as newName1 ”. The direction is left-to-right, as if you had written:
Confusingly, the colon here does not indicate the type. The type, if you specify it, still needs to be written after the entire destructuring:
Default values let you specify a default value in case a property is undefined:
In this example the b? indicates that b is optional, so it may be undefined . keepWholeObject now has a variable for wholeObject as well as the properties a and b , even if b is undefined.
Destructuring also works in function declarations. For simple cases this is straightforward:
But specifying defaults is more common for parameters, and getting defaults right with destructuring can be tricky. First of all, you need to remember to put the pattern before the default value.
The snippet above is an example of type inference, explained earlier in the handbook.
Then, you need to remember to give a default for optional properties on the destructured property instead of the main initializer. Remember that C was defined with b optional:
Use destructuring with care. As the previous example demonstrates, anything but the simplest destructuring expression is confusing. This is especially true with deeply nested destructuring, which gets really hard to understand even without piling on renaming, default values, and type annotations. Try to keep destructuring expressions small and simple. You can always write the assignments that destructuring would generate yourself.
The spread operator is the opposite of destructuring. It allows you to spread an array into another array, or an object into another object. For example:
This gives bothPlus the value [0, 1, 2, 3, 4, 5] . Spreading creates a shallow copy of first and second . They are not changed by the spread.
You can also spread objects:
Now search is < food: "rich", price: "$$", ambiance: "noisy" >. Object spreading is more complex than array spreading. Like array spreading, it proceeds from left-to-right, but the result is still an object. This means that properties that come later in the spread object overwrite properties that come earlier. So if we modify the previous example to spread at the end:
Then the food property in defaults overwrites food: «rich» , which is not what we want in this case.
Object spread also has a couple of other surprising limits. First, it only includes an objects’ own, enumerable properties. Basically, that means you lose methods when you spread instances of an object:
Second, the TypeScript compiler doesn’t allow spreads of type parameters from generic functions. That feature is expected in future versions of the language.
The TypeScript docs are an open source project. Help us improve these pages by sending a Pull Request ❤