TypeScript - быстрое погружение
Author: Kirill Ostapenko @khbfr
Эта книга в основном сборник самого главного и компиляция разрозненной информации из
открытых источников
TypeScript — это «разновидность» или «вариант» JavaScript. Отношения между
TypeScript (TS) и JavaScript (JS) довольно уникальны среди современных языков
программирования, поэтому более подробное изучение этих отношений поможет вам
понять, как TypeScript расширяет JavaScript.
Оглавление
JavaScript?
TypeScript
Install via NPM
TS Compilation
Basic data types
Boolean
Number
String
null
undefined
Object
Variables
var
Scope
let
Block scope
Const
let or const?
Operators
Logic operators
Terms
if...else
else if
Loops and iterations
Continue
Break
Iterables
for..of
Arrays
length
Functions
Declaring and calling a function
Callback
Adding Types to a Function
Type inference
Optional parameters
Rest parameters
Object Oriented Programming
Encapsulation
Hidden and public class members
Inheritance
Polymorphism
Classes
Class fields
Methods
Constructors
Access Modifiers
public
private
protected
readonly
Parameter Properties
Accessors
Abstract classes
Abstract methods
Abstract fields
Interfaces
Interface Implementation
Difference between static part and class instance
Interface Extends
Interfaces Extending Classes
Asynchronous programming
Sequence of asynchronous operations
Promises
What is a promise in TypeScript?
Sequential execution with .then
Error handling in try/catch
throw
Example: Throwing an Object as an Exception
Decorators for functions
Decorators for classes
Decorators for fields or class properties
NPM
package.json
Project setup
NPM scripts
Dependencies and devDependencies
Installing packages
npm ci
npm audit
Public License Terms for Electronic Versions
JavaScript?
JavaScript (также известный как ECMAScript или JS) начал свою жизнь как простой язык
сценариев для браузеров. В то время, когда он был изобретен, предполагалось, что он будет
использоваться для коротких фрагментов кода, встроенных в веб-страницу — написание
более нескольких десятков строк кода было бы несколько необычным. Из-за этого ранние веб-
браузеры выполняли такой код довольно медленно. Однако со временем JS становился все
более и более популярным, и веб-разработчики начали использовать его для создания
интерактивных приложений.
Разработчики веб-браузеров отреагировали на это возросшее использование JS,
оптимизировав свои механизмы выполнения (динамическая компиляция) и расширив
возможности его использования (добавив API), что, в свою очередь, заставило веб-
разработчиков использовать его еще больше. На современных веб-сайтах ваш браузер часто
запускает приложения, содержащие сотни тысяч строк кода. Это долгий и постепенный рост
«Интернета», начиная с простой сети статических страниц и превращаясь в платформу для
многофункциональных приложений всех видов.
Более того, JS стал достаточно популярным, чтобы его можно было использовать вне
контекста браузеров, например, для реализации JS-серверов с использованием node.js или
deno. Способность JS «работать где угодно» делает его привлекательным выбором для
кроссплатформенной разработки. В наши дни многие разработчики используют только
JavaScript для программирования всего своего стека!
Подводя итог, у нас есть язык, который был разработан для быстрого использования, а
затем вырос до полноценного инструмента для написания приложений с миллионами строк.
TypeScript программа компилируется в обычный JavaScript-код, который может
выполняться в любом браузере или в среде Node.js. Этот код будет понятен любому JS-
движку, который поддерживает стандарт ECMAScript 3 или более
TypeScript
Обнаружение ошибок в коде без его запуска называется статической проверкой.
Определение того, что является ошибкой, а что нет, на основе типов обрабатываемых
значений называется проверкой статического типа.
TypeScript проверяет программу на наличие ошибок перед выполнением и делает это на
основе типов значений, это средство проверки статического типа.
При статической типизации финальные типы переменных и функций устанавливаются на
этапе компиляции. Компилятор еще до запуска программы исправляет ваши ошибки при
несоответствии типов.
JavaScript — язык с динамической типизацией.
В динамической типизации все типы определяются уже во время выполнения программы. И
если вы допустили ошибку, то узнаете об этом только при выполнении. Поэтому при
динамической типизации очень важно уделять особое внимание проверкам и перехвату
ошибок.
Синтаксис
TypeScript — это язык, который является надмножеством JavaScript: поэтому синтаксис JS
является равным TS. Синтаксис относится к тому, как мы пишем текст для формирования
программы.
Вы можете взять любой работающий код JavaScript и поместить его в файл TypeScript, не
беспокоясь о том, как именно он написан. Из этого следует что вы не можете выучить
TypeScript, не зная JavaScript! Синтаксис TypeScript и поведение во время выполнения
совпадают с JavaScript, поэтому все, что вы узнаете о JavaScript, одновременно поможет вам
изучить TypeScript.
Программистам доступно множество ресурсов для изучения JavaScript; вам не следует
игнорировать эти ресурсы, если вы пишете TypeScript. Например, помеченных вопросов с
JavaScript на StackOverflow примерно в 20 раз больше чем с TypeScript, но все эти вопросы
также относятся и к TypeScript.
Если вы обнаружите, что ищете что-то вроде «как отсортировать список в TypeScript»,
помните: TypeScript — это надмножество JavaScript с проверкой типов во время компиляции.
Способ сортировки списка в TypeScript такой же, как и в JavaScript.
Если вы найдете ресурс, который напрямую использует TypeScript, это тоже прекрасно, но
не ограничивайте себя мыслью, что вам нужны ответы, специфичные для TypeScript, на
повседневные вопросы о том, как выполнять задачи во время выполнения.
Есть две популярные IDE для JavaScript и TypeScript:
Visual Studio Code (бесплатно)
WebStorm (платно)
Важно знать, что Visual Studio Code обрабатывает исходный код TypeScript двумя
независимыми способами:
Проверка открытых файлов на наличие ошибок: это делается через так называемый
языковой сервер. Языковые серверы существуют независимо от конкретных редакторов и
предоставляют Visual Studio Code услуги, связанные с языком: обнаружение ошибок,
рефакторинг, автодополнение и т.д. Связь с серверами происходит по протоколу, основанному
на JSON-RPC (RPC удаленные вызовы процедур). Независимость, обеспечиваемая этим
протоколом, означает, что серверы могут быть написаны практически на любом языке
программирования.
Важно помнить: языковой сервер только перечисляет ошибки для открытых в данный
момент файлов и не компилирует TypeScript, он только анализирует его статически.
Сборка (компиляция файлов TypeScript в файлы JavaScript): Здесь у нас есть два варианта:
Мы можем запустить инструмент сборки через внешнюю командную строку. Например,
компилятор TypeScript tsc имеет режим --watch, который отслеживает входные файлы и
компилирует их в выходные файлы всякий раз, когда они изменяются. Как следствие, всякий
раз, когда мы сохраняем файл TypeScript в среде IDE, мы немедленно получаем
соответствующий выходной файл (файлы).
Мы можем запустить tsc из кода Visual Studio Code. Для этого он должен быть установлен
либо внутри проекта, над которым мы сейчас работаем, либо глобально (через менеджер
пакетов NPM).
Чтобы начать работать с TypeScript, установим необходимый инструментарий. Установить
TypeScript можно двумя способами: через пакетный менеджер NPM или как плагин к Visual
Studio
Install via NPM
Для установки через NPM вначале естественно необходимо установить Node.js (если он
ранее не был установлен). После установки Node.js необходимо запустить следующую
команду в командной строке (Windows) или терминале (Linux):
npm install -g typescript
При установке на MacOS также требуется ввести команду sudo. При вводе данной команды
терминал запросит логин и пароль пользователя для установки пакета:
sudo npm install -g typescript
Вполне возможно, что ранее уже был установлен TS. В этом случае его можно обновить до
последней версии с помощью команды
npm update -g typescript
Для проверки версии необходимо ввести команду
tsc -v
TS Compilation
Создадим файл app.ts причем именно app.ts, а не app.js, то есть файл кода typescript. Это
также обычный текстовый файл, который имеет расширение .ts. И определим следующее
содержание:
class User {
public name: string;
public constructor(name: string) {
this.name = name;
}
}
const tom: User = new User("Том");
console.log(tom.name)
Что здесь происходит? Сначала определяет класс User. Этот класс имеет поле name,
которое представляет тип string, то есть строку. Для передачи данных этому полю определен
специальный метод - constructor, который принимает через параметр name некоторую строку и
передает ее в поле name:
class User {
public name: string;
public constructor(name: string) {
this.name = name;
}
}
Далее мы подробнее разберем создание и использование классов.
Cоздаем константу tom, которая представляет этот класс:
js const tom: User = new User("Том");
Иначе говоря, константа tom представляет некоторого пользователя, у которого имя "Том".
Затем выводим данные в консоль
js console.log(tom.name)
Сохраним и скомпилируем этот файл. Для этого в командной строке/терминале с помощью
команды cd перейдем к каталогу, где расположен файл app.ts. И для компиляции выполним
следующую команду:
js tsc app.ts
После компиляции в каталоге проекта создается файл app.js, который будет выглядеть так:
let User = /\\ @class \/* (function () {
function User(name) {
this.name = name;
}
return User;
}());
let tom = new User("Том");
console.log(tom.name)
И теперь мы можем командой node app.js запустить файл и увидеть в в командной строке/
терминале результат работы нашего приложения
Basic data typ es
Typescript является языком со статической типизацией. Тип не может быть изменен в ходе
выполнения программы. Это позволяет снизить количество ошибок и выявить многие еще на
этапе компиляции.
В Typescript есть несколько простых типов данных: numbers (числа), strings (строки), Object
Types (типы объектов), boolean (логический). Он поддерживает все типы, которые есть в
Javascript, дополняя удобным типом перечислений (enum).
Boolean
Наиболее базовым типом является логический true/false, который в Javascript и Typescript
называется boolean.
let isDone: boolean = false;
Number
Как и в Javascript, тип number в Typescript являются числом с плавающей точкой. Кроме
десятичного и шестнадцатеричного формата, поддерживаются бинарный и восьмеричный,
введенные в ECMAScript 2015.
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
String
Еще одна важная часть программ это текстовые данные. Как и в других языках, в
Typescript используется то же обозначение "string". Как и Javascript, в Typescript
используются двойные (") или одинарные (') кавычки для обрамления текстовых данных.
let name: string = "bob";
name = 'smith';
Вы также можете использовать строки с шаблонами, которые могут быть многострочными
и иметь встроенные выражения. Эти строки окружаются обратными апострофами или
кавычками(`) и встроенные выражения обозначаются как ${ expr }.
let name: string = Gene;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }.
I'll be ${ age + 1 } years old next month.`
Эквивалент этого объявления sentence:
let sentence: string = "Hello, my name is " + name + ".\n\n" +
"I'll be " + (age + 1) + " years old next month."
null
В JavaScript, null является примитивом, и в контексте логических операций, рассматривается
как ложное (falsy).
function getVowels(str) {
const m = str.match(/[aeiou]/gi);
if (m === null) {
return 0;
}
return m.length;
}
console.log(getVowels('sky'));
// expected output: 0
null — это специальное значение в JavaScript, которое представляет отсутствующий
объект. Оператор строгого равенства определяет, является ли null переменной: variable ===
null. Оператор typeof полезен для определения типа переменной (число, строка, логическое
значение). Однако typeof вводит в заблуждение в случае null: typeof null оценивается, как
‘object’.
null и undefined в какой-то мере эквивалентны, тем не менее, null представляют собой
отсутствующий объект, а undefined неинициализированное состояние.
По возможности избегайте возврата null или установки для переменных null. Такая
практика приводит к распространению значений null и проверок на null. Вместо этого
попробуйте использовать объекты со свойствами по умолчанию или выдавать ошибки.
undefined
Примитивное значение. Автоматически присваивается переменным, которые были только
объявлены или аргументам, для которых не были установлены значения.
function test(t) {
if (t === undefined) {
return 'Undefined value!';
}
return t;
}
let x;
console.log(test(x));
// expected output: "Undefined value!"
Object
Объект может быть создан с помощью фигурных скобок {…} с необязательным списком
свойств. Свойство – это пара «ключ: значение», где ключ – это строка (также называемая
«именем свойства»), а значение может быть чем угодно.
При использовании литерального синтаксиса {...} мы сразу можем поместить в объект
несколько свойств в виде пар «ключ: значение»:
let user = { // объект
name: "John", // под ключом "name" хранится значение "John"
age: 30
};
// под ключом "age" хранится значение 30
Variables
let и const - относительно новые типы объявления переменных в JavaScript. Let похож на
var в некотором смысле, но позволяет избежать некоторые из общих ошибок, с которыми
сталкиваются в JavaScript. Const это расширение let, которое предотвращает переопределение
переменных.
Та к как TypeScript является надстройкой над Javascript, язык также поддерживает let и
const.
var
Объявление переменной в JavaScript всегда происходит с помощью ключевого слова var.
js 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(); // возвращает 11;
Scope
Объявление var имеет несколько странных правил области видимости для тех, кто
использует другие языки программирования.
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true); // returns '10'
f(false); // returns 'undefined'
Переменная x была объявлена внутри блока if, и мы можем получить к ней доступ вне
этого блока. Это потому что объявления var доступны внутри содержащей их функции,
модуля, пространства имен(namespace) или глобальной области видимости несмотря на блок,
в котором они содержатся.
Эти правила области видимости могут вызвать несколько типов ошибок. Одна из проблем -
это то, что не является ошибкой объявление переменной несколько раз:
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 имеет области видимости внутри функции sumMatrix. Опытные
разработчики знают, что похожие ошибки проскальзывают при code review.
let
Сейчас мы уже понимаем, что var имеет некоторые проблемы, именно поэтому появился
новый способ объявления переменных let. Они записываются точно также, как и объявления
var.
let hello = "Hello!";
Ключевое различие не в синтаксисе, а в семантике, в которую мы сейчас
Blo ck scope
Когда переменная объявляется с использованием let, она используется в режиме блочной
области видимости. В отличие от переменных, объявленных с помощью var, чьи области
видимости распространяются на всю функцию, в которой они находятся, переменные блочной
области видимости не видимы вне их ближайшего блока или же цикла for.
function f(input: boolean) {
let a = 100;
if (input) {
// Здесь мы видим переменную 'a'
let b = a + 1;
return b;
}
// Ошибка: 'b' не существует в этом блоке
return b;
}
Здесь мы имеем две локальные переменные a и b. Область видимости a ограничена телом
функции f, в то время как область b ограничена блоком условия if.
Переменные, объявленные в блоке catch имеют те же правила видимости.
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
// Error: 'e' doesn't exist here
console.log(e);
Другое свойство переменных блочной области видимости - к ним нельзя обратиться перед
тем, как они были объявлены. При том, что переменные блочной области видимости
представлены везде в своем блоке, в каждой точке до их объявления находится мертвая зона.
Это просто такой способ сказать, что вы не можете получить к ним доступ до утверждения
let и, к счастью, TypeScript напомнит вам об этом.
a++; // неверно использовать 'a' до ее объявления;
let a;
Однако, вы все еще можете захватить (замкнуть) переменную с блочной областью
видимости до ее объявления. Правда, попытка вызвать такую функцию до ее объявления
приведет к ошибке. Если вы компилируете в стандарт ES2015, это вызовет ошибку; тем не
менее, прямо сейчас TypeScript разрешает это и не будет указывать на ошибку.
function foo() {
// okay to capture 'a'
return a;
}
// illegal call 'foo' before 'a' is declared
// runtimes should throw an error here
Const
Объявления const - это еще один способ объявления переменных.
const numLivesForCat = 9;
Они такие же как и let, только, согласно их названию, их значение не может быть изменено
после того, как им однажды уже присвоили значение. Другими словами, к ним применимы все
правила области видимости let, но вы не можете их переназначить. Значение, с которым они
связаны, является неизменным
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
}
// Ошибка
kitty = {
name: "Danielle",
numLives: numLivesForCat
};
// Все хорошо
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
Несмотря на то, что переменная была объявлена как const, ее внутреннее состояние все еще
может быть изменено. К счастью, TypeScript позволяет вам определить свойства объекта
доступными только на чтение: readonly.
let or const?
У нас есть два способа объявления с похожими правилами их области видимости, поэтому
сам собой напрашивается вопрос о том, какой использовать. Ответ будет таким же, как и на
большинство широких вопросов: это зависит от обстоятельств.
Применяя принцип наименьшего уровня привилегий, все объявления переменных, которые
вы в дальнейшем не планируете менять, должны использовать const. Объясняется это тем,
что если переменная не должна изменять свое значение, другие разработчики, которые
работают над тем же кодом, не должны иметь возможность записи в объект. Это должно
быть позволено только в случае реальной необходимости переназначения переменной.
Использование const делает код более предсказуемым и понятным при объяснении потока
данных.
Использование var считается антипаттерном.
Антипаттерн — это распространенный подход к решению класса часто встречающихся
проблем, который является неэффективным, рискованным или непродуктивным.
Operators
В JavaScript есть следующие типы операторов. Данный подраздел описывает каждый тип и
содержит информацию об их приоритетах друг над другом.
• Операторы присваивания
• Операторы сравнения
• Арифметические операторы
• Битовые (поразрядные) операторы
• Логические операторы
• Строковые операторы
• Условный (тернарный) оператор
• Оператор запятая
• Унарные операторы
• Операторы отношения
• Приоритет операторов
Оператор сравнения сравнивает свои операнды и возвращает логическое значение,
базируясь на истинности сравнения. Операнды могут быть числами, строками, логическими
величинами или объектами. Строки сравниваются на основании стандартного
лексикографического порядка, используя Unicode-значения. В большинстве случаев, если
операнды имеют разный тип, то JavaScript пробует преобразовать их в тип, подходящий для
сравнения. Такое поведение обычно происходит при сравнении числовых операндов.
Единственным исключением из данного правила является сравнение с использованием
операторов === и !==, которые производят строгое сравнение на равенство или неравенство.
Эти операторы не пытаются преобразовать операнды перед их сравнением.
Logic operators
Логические операторы обычно используются с булевыми (логическими) значениями; при
этом возвращаемое ими значение также является булевым. Однако операторы && и ||
фактически возвращают значение одного из операндов, поэтому, если эти операторы
используются с небулевыми величинами, то возвращаемая ими величина также может быть
не булевой. Логические операторы описаны в следующей таблице.
Логическое И &&
expr1 && expr2
(Логическое И) Возвращает операнд expr1, если он может быть преобразован в false; в
противном случае возвращает операнд expr2. Та ким образом, при использовании булевых
величин в качестве операндов, оператор && возвращает true, если оба операнда true; в
противном случае возвращает false.
Логическое ИЛИ ||
expr1 || expr2
(Логическое ИЛИ) Возвращает операнд expr1, если он может быть преобразован в true; в
противном случае возвращает операнд expr2. Та ким образом, при использовании булевых
величин в качестве операндов, оператор || возвращает true, если один из операндов true; если
же оба false, то возвращает false.
Логическое НЕ !
!expr
(Логическое НЕ) Возвращает false, если операнд может быть преобразован в true; в
противном случае возвращает true.
Примерами выражений, которые могут быть преобразованы в false являются: null, 0, NaN,
пустая строка ("") или undefined.
Примеры использования оператора && (логическое И).
1\. let a1 = true && true; // t && t возвращает true
2\. let a2 = true && false; // t && f возвращает false
3\. let a3 = false && true; // f && t возвращает false
4\. let a4 = false && (3 == 4); // f && f возвращает false
5\. let a5 = "Cat" && "Dog"; // t && t возвращает Dog
6\. let a6 = false && "Cat"; // f && t возвращает false
7\. let a7 = "Cat" && false; // t && f возвращает false
Примеры использования оператора || (логическое ИЛИ).
1\. let o1 = true || true; // t || t возвращает true
2\. let o2 = false || true; // f || t возвращает true
3\. let o3 = true || false; // t || f возвращает true
4\. let o4 = false || (3 == 4); // f || f возвращает false
5\. let o5 = "Cat" || "Dog"; // t || t возвращает Cat
6\. let o6 = false || "Cat"; // f || t возвращает Cat
7\. let o7 = "Cat" || false; // t || f возвращает Cat
Примеры использования оператора ! (логическое НЕ).
1\. let n1 = !true; // !t возвращает false
2\. let n2 = !false; // !f возвращает true
3\. let n3 = !"Cat"; // !t возвращает false
Ter ms
if...else
Инструкция if выполняет инструкцию, если указанное условие выполняется (истинно). Если
условие не выполняется (ложно), то может быть выполнена другая инструкция.
\>if (условие)
\>инструкция1
\>[else
\>инструкция2]
if...else
1\. if (cipher\_char === from\_char) {
2\. result = result + to\_char;
3\. x++;
4\. } else {
5\. result = result + clear\_char;
6\. }
else if
Обратите внимание, что в JavaScript нет синтаксиса elseif. Однако вы можете записать его с
пробелом между else и if:
1\. if (x > 5) {
2\. } else if (x > 50) {
3\. } else {
4\. }
Loops and iterations
Вы можете представить цикл в виде компьютеризированной версии игры, где вы говорите
кому-то сделать X шагов в одном направлении, затем Y шагов в другом; для примера, идея
игры "Иди 5 шагов на восток" может быть выражена в виде цикла:
1\. for (let step = 0; step < 5; step++) {
2\. // Запускается 5 раз, с шагом от 0 до 4.
3\. console.log('Идём 1 шаг на восток');
4\. }
Существует множество различных видов циклов, но все они по сути делают тоже самое:
повторяют какое-либо действие несколько раз (не забывайте про нулевой раз повторения,
отсчёт в массиве начинается с 0). Различные по строению циклы предлагают разные способы
для определения начала и окончания цикла. Для различных задач программирования
существуют свои операторы цикла, с помощью которых они решаются намного проще.
Операторы предназначенные для организации циклов в JavaScript:
• Цикл\_for
• Цикл\_do...while
• Цикл\_while
• Метка\_(label)
• break
• continue
• for...in
• for...of
Цикл for повторяет действия, пока не произойдёт какое-либо специальное событие
завершения цикла. Оператор for в JavaScript аналогичен оператору for в Java и C. Объявление
оператора for выглядит следующим образом:
for ([начало]; [условие]; [шаг]) выражения
При его выполнении происходит следующее:
1\. Выполняется выражение начало, если оно указано. Это выражение обычно
инициализирует один или несколько счётчиков, но синтаксис позволяет выражению
быть любой сложности. Та кже используется для объявления переменных.
2\. Выполняется условие. Если условие истинно, то выполняются выражения. Если оно
ложно, цикл for прерывается. Если же условие полностью пропущено, то оно считается
истинным.
3\. Выполняются выражения. Чтобы выполнить несколько выражений, используются блок-
выражение { ... } для группировки выражений.
4\. Обновляется шаг, если он есть, а затем управление возвращается к шагу 2.
Цикл while выполняет выражения пока условие истинно. Выглядит он так:
5\. while (i < 3) { // Выполнять код, пока значение переменной i меньше 3
6\. alert("i: " + i);
7\. i++; // Увеличиваем значение переменной i
8\. }
Если условие становится ложным, выражения в цикле перестают выполняться и управление
переходит к выражению после цикла.
Условие проверяется на истинность до того, как выполняются выражения в цикле. Если
условие истинно, выполняются выражения, а затем условие проверяется снова.
Если условие ложно, выполнение приостанавливается и управление переходит к выражению
после while.
Чтобы использовать несколько выражений, используйте блок выражение { ... }, чтобы
сгруппировать их.
Continue
Оператор continue используется, чтобы шагнуть на шаг вперёд в циклах while, do-while, for
или перейти к метке.
• Когда вы используете continue без метки, он прерывает текущую итерацию циклов
while, do-while и for и продолжает выполнение цикла со следующей итерации. В отличие
от break, continue не прерывает выполнение цикла полностью. В цикле while он прыгает
к условию. А в for увеличивает шаг.
• Когда вы используете continue с меткой, он применяется к циклу с этой меткой.
Синтаксис continue может выглядеть так:
1\. continue;
2\. continue Метка;
1\. for (let i = 0; i <= 10; i++) {
2\. if ((i % 2) != 0) continue; // Если значение переменной не чётное,
3\.
// завершить текущую итерацию
4\. alert(i);
5\. }
Break
Используйте оператор break, чтобы прерывать цикл или переключать управление.
• Когда вы используете break, он прерывает циклы while, do-while и for или сразу
переключает управление к следующему выражению.
Синтаксис оператора может быть таким:
1\. for (var i = -2; i <= 2; i++) {
2\. if (i > 0) break; // Если значение переменной i станет положительным,
3\.
// завершить итерацию и выйти из цикла
}
Первая форма синтаксиса прерывает цикл совсем или переключает управление; вторая
прерывает специально обозначенное выражение.
Iterables
Итерируемым является такой объект, который содержит реализацию свойства
Symbol.iterator. Некоторые встроенный типы, такие как Array, Map, Set, String, Int32Array,
Uint32Array и т.д., содержат уже реализованное свойство Symbol.iterator. Взятая от объекта
функция Symbol.iterator должна возвратить список значений для организации цикла.
for..of
Для перебора итерируемого объекта в цикле for..of, на нём вызывается свойство
Symbol.iterator. Вот простой пример прохождения цикла for..of по массиву:
let someArray = [1, "string", false];
for (let entry of someArray) {
console.log(entry); // 1, "string", false
}
for..of против for..in
И for..of, и for..in используются для перебора списков. При этом значения, по которым
выполняется обход, различаются. for..in возвращает список ключей итерируемого объекта,
когда как for..of возвращает список значений его числовых свойств.
Следующий пример демонстрирует различие:
let list = [4, 5, 6];
for (let i in list) {
console.log(i); // "0", "1", "2",
}
for (let i of list) {
console.log(i); // "4", "5", "6"
}
Ещё одно различие заключается в том, что for..in может использоваться для обхода
любого объекта, являясь средством инспектирования его свойств. А for..of в первую очередь
предназначен для получения значений. Та кие встроенные объекты, как Map и Set, реализуют
свойство Symbol.iterator, предоставляя с его помощью доступ к хранимым значениям.
let pets = new Set(["Cat", "Dog", "Hamster"]);
pets["species"] = "mammals";
for (let pet in pets) {
console.log(pet); // "species"
}
for (let pet of pets) {
console.log(pet); // "Cat", "Dog", "Hamster"
}
Arrays
Массивы (Array) являются спископодобными объектами, чьи прототипы содержат методы
для операций обхода и изменения массива. Ни размер JavaScript-массива, ни типы его
элементов не являются фиксированными. Поскольку размер массива может увеличиваться и
уменьшаться в любое время, то нет гарантии, что массив окажется плотным. То есть, при
работе с массивом может возникнуть ситуация, что элемент массива, к которому вы
обратитесь, будет пустым и вернёт undefined. В целом, это удобная характеристика; но если
эта особенность массива не желательна в вашем специфическом случае, вы можете
рассмотреть возможность использования типизированных массивов.
Создание массива
1\. let fruits = ['Яблоко', 'Банан'];
2\. console.log(fruits.length);
3\. // 2
Добавление элемента в начало массива
1\. let newLength = fruits.unshift('Клубника') // добавляет в начало
2\. // ["Клубника", "'Яблоко', 'Банан']];
Добавление элемента в конец массива
var sports = ['футбол', 'бейсбол'];
var total = sports.push('американский футбол', 'плавание');
console.log(sports); // ['футбол', 'бейсбол', 'американский футбол', 'плавание']
Удаление элемента с определенным индексом
let removedItem = fruits.splice(pos, 1); // так можно удалить элемент
// ["Клубника", "'Банан'"]
length
Свойство length является целым числом с положительным знаком и значением, меньшим
чем 2 в степени 32 (232).. Некоторые встроенные методы массива (например, join, slice,
indexOf и т.д.) учитывают значение свойства length при своем вызове. Другие методы
(например, push, splice и т.д.) в результате своей работы также обновляют свойство length
массива.
1\. let fruits = [];
2\. fruits.push('банан', 'яблоко', 'персик');
3\.
4\. console.log(fruits.length); // 3
Если индекс выходит за пределы текущих границ массива, свойство length
соответствующим образом обновляется.
Functions
Функции — фундаментальные строительные блоки каждого приложения на JavaScript. С их
помощью строятся слои абстракции, сокрытие данных и модули. Хотя в TypeScript есть и
классы, и пространства имен, и модули, функции по-прежнему играют ключевую роль в
описании того, как все работает.
Кроме того, TypeScript добавляет несколько новых возможностей к стандартным
JavaScript-функциям, и делает работу с ними проще.
То чно так же, как и в JavaScript, функции в TypeScript могут создаваться и как
именованные, так и анонимные. Это дает возможность выбрать подход, который лучше
подходит конкретному приложению: создается ли группа функций для API, либо функция,
которая нужна лишь для того, чтобы быть аргументом для другой функции.
Как эти два варианта выглядят в JavaScript:
// Именованная функция
function add(x, y) {
return x + y;
}
// Анонимная функция
let myAdd = function(x, y) { return x+y; };
Как и в JavaScript, функции могут обращаться к переменным вне своего тела. Когда такое
происходит, говорят, что функция "захватывает" переменные.
let z = 100;
function addToZ(x, y) {
return x + y + z;
}
Declaring and calling a function
Существует три способа объявления функции: Function Declaration, Function Expression и
Named Function Expression.
Function Declaration (сокращённо FD) – это "классическое" объявление функции. В
JavaScript функции объявляются с помощью литерала функции. Синтаксис объявления FD:
function идентификатор (параметры) { инструкции }
Литерал функции состоит из следующих четырёх частей:
1\. Ключевое слово function.
2\. Обязательный идентификатор, определяющий имя функции. В качестве имени функции
обычно выбирают глагол, т. к. функция выполняет действие.
3\. Пара круглых скобок вокруг списка из нуля или более идентификаторов, разделяемых
запятыми. Данные идентификаторы называются параметрами функции.
4\. Те ло функции, состоящее из пары фигурных скобок, внутри которых располагаются
инструкции. Те ло функции может быть пустым, но фигурные скобки должны быть
указаны всегда.
Простой пример:
function sayHi() {
alert("Hello");
}
Встречая ключевое слово function интерпретатор создаёт функцию и затем присваивает
ссылку на неё переменной с именем sayHi (переменная с данным именем создаётся
интерпретатором автоматически).
Обратившись к переменной sayHi можно увидеть, что в качестве значения там находится
функция (на самом деле ссылка на неё):
alert(sayHi); // function sayHi() { alert("Hello"); }
Function Expression (сокращённо FE) – это объявление функции, которое является частью
какого-либо выражения (например присваивания).
Синтаксис объявления FE
function (параметры) { инструкции }
Простой пример:
let sayHi = function () {
alert("Hello");
};
Функцию FE иначе ещё называют "анонимной функцией".
Named Function Expression (сокращённо NFE) – это объявление функции, которое является
частью какого-либо выражения (например присваивания). Синтаксис объявления NFE:
function идентификатор (параметры) { инструкции }
Простой пример:
let sayHi = function foo() {
alert("Hello");
};
Объявления FE и NFE обрабатываются интерпретатором точно так же, как и объявление
FD: интерпретатор создаёт функцию и сохраняет ссылку на неё в переменной sayHi.
Программный код, расположенный в теле функции, выполняется не в момент объявления
функции, а в момент её вызова. Для вызова функции используется оператор () (вызов
функции):
function sayHi() {
alert("Hello");
}
let sayHi2 = function () {
alert("Hello2");
};
let sayHi3 = function foo() {
alert("Hello3");
};
sayHi(); // "Hello"
sayHi2(); // "Hello2"
sayHi3(); // "Hello3"
Разница между представленными тремя объявлениями заключается в том, что функции,
объявленные как FD, создаются интерпретатором до начала выполнения кода (на этапе
анализа), поэтому их можно вызывать (в той области видимости где они объявлены) до
объявления:
// Вызов функции до её объявления в коде верхнего уровня
foo();
function foo() {
alert("Вызов функции foo() в глобальной области видимости.");
// Вызов функции до её объявления в области видимости функции
bar();
function bar() {
alert("Вызов функции bar() в области видимости функции.");
}
}
ункции, объявленные как FE или NFE, создаются в процессе выполнения кода, поэтому их
можно вызывать только после того как они объявлены:
// sayHi(); // Ошибка. Функция sayHi ещё не существует
let sayHi = function () {
alert("Hello!");
};
sayHi();
Функции, объявленные внутри блока, находятся в блочной области видимости:
// foo(); // Ошибка. Функция не объявлена.
{
foo(); // 1
function foo() {
console.log(1);
}
}
foo();
// Ошибка. Функция не объявлена.
В отличие от FE, функция, объявленная как NFE, имеет возможность обращаться к себе по
имени при рекурсивном вызове. Имя функции доступно только внутри самой функции:
(function sayHi(str) {
if (str) { return; }
sayHi("hi"); // Имя доступно внутри функции
})();
sayHi();
// Ошибка. Функция не объявлена
Callback
Функция обратного вызова – это функция, которая передаётся в качестве аргумента
другой функции для последующего её вызова.
Функции обратного вызова часто используются, в качестве обработчиков событий.
Ниже приведён пример функции, принимающей в качестве своего аргумента ссылку на
другую функцию для её последующего вызова:
function foo(callback) { return callback(); }
foo (function() { alert("Hello!"); });
Этот пример наглядно демонстрирует принцип действия обратного вызова.
С помощью инструкции return функция может возвратить некоторое значение (результат
работы функции) программе, которая её вызвала. Возвращаемое значение передаётся в точку
вызова функции.
Инструкция return имеет следующий синтаксис:
return выражение;
В программу возвращается не само выражение, а результат его вычисления. Для
дальнейшего использования возвращаемого значения, результат выполнения функции можно
присвоить к примеру переменной:
function calc(a) {
return a \* a;
}
let x = calc(5);
alert(x); // 25
Инструкция return может быть расположена в любом месте функции. Как только будет
достигнута инструкция return, функция возвращает значение и немедленно завершает своё
выполнение. Код, расположенный после инструкции return, будет проигнорирован:
function foo() {
return 1;
alert('Не выполнится');
}
let x = foo();
alert(x); // 1
Внутри функции можно использовать несколько инструкций return:
function check(a, b) {
if(a > b) return a;
else return b;
}
alert(check(3, 5)); // 5
Если инструкция return не указана или не указано возвращаемое значение, то функция
вернёт значение undefined:
function bar() {}
function foo() { return; }
alert(bar()); // undefined. Инструкция return не указана
alert(foo()); // undefined. Возвращаемое значение не указано
Adding Types to a Function
Добавим к функции из предыдущих простых примеров типы:
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x+y; };
Добавлять типы можно к каждому параметру, а также и к самой функции, чтобы указать
тип возвращаемого значения. TypeScript умеет сам выводить тип возвращаемого значения,
анализируя инструкции return, поэтому зачастую можно не указывать его явно.
Пишем тип функции
Теперь, когда мы добавили к функции типы, можно описать ее полный тип, собрав его по
кусочкам из определения:
let myAdd: (x: number, y: number)=>number =
function(x: number, y: number): number { return x+y; };
Тип функции состоит из таких же двух частей: типа аргументов и типа возвращаемого
значения. Когда записывается полный тип функции, указывать необходимо обе эти части.
Типы параметров записываются так же, как и список параметров, и каждому параметру
присваивается имя и тип. Имена здесь нужны только для удобства чтения, можно было бы
написать, к примеру, вот так:
let myAdd: (baseValue:number, increment:number) => number =
function(x: number, y: number): number { return x + y; };
Если типы параметров совпадают, то тип считается подходящим для функции, и не важно,
какие имена были даны параметрам в описании типа функции.
Вторая часть типа функции — тип возвращаемого значения. На него указывает толстая
стрелка (=>) между параметрами и типом возвращаемого значения. Как писалось выше, эта
часть необходима, и поэтому, если функция не возвращает ничего, в качестве типа
возвращаемого значения нужно указать void.
Type inference
Экспериментируя со следующим примером, можно заметить, что компилятор TypeScript
способен разобраться с типами, если они указаны лишь в одной половине выражения:
// myAdd имеет полный тип функции
let myAdd = function(x: number, y: number): number { return x + y; };
// У параметров 'x' и 'y' — тип "number"
let myAdd: (baseValue:number, increment:number) => number =
function(x, y) { return x + y; };
Это называется контекстной типизацией — одним из видов выведения типов. Такая
особенность позволяет тратить меньше усилий на то, чтобы добавить типы в программу.
Optional parameters
В TypeScript считается, что каждый параметр функции обязателен. Это не значит, что ей
нельзя передать null или undefined: это означает, что при вызове функции компилятор
проверит, задал ли пользователь значение для каждого ее параметра. Кроме того,
компилятор считает, что никакие параметры, кроме указанных, не будут передаваться. Проще
говоря, число передаваемых параметров должно совпадать с числом параметров, которые
ожидает функция.
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob");
let result2 = buildName("Bob", "Adams", "Sr."); // ошибка, слишком много параметров
let result3 = buildName("Bob", "Adams"); // в самый раз
// ошибка, слишком мало параметров
В JavaScript все параметры необязательны, и пользователи могут пропускать их, если
нужно. В таких случаях значение пропущенных параметров принимается за undefined. В
TypeScript тоже можно добиться этого: для этого в конце параметра, который нужно сделать
необязательным, добавляется ?. К примеру, мы хотим сделать необязательным lastName из
предыдущего примера:
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob");
let result2 = buildName("Bob", "Adams", "Sr."); // ошибка, слишком много параметров
let result3 = buildName("Bob", "Adams"); // в самый раз
// сейчас все правильно
Все необязательные параметры должны идти после обязательных. Если бы первый
параметр (firstName) нужно было сделать опциональным вместо lastName, то порядок
параметров в функции пришлось бы изменить, чтобы firstName оказался последним.
Та кже TypeScript позволяет указать для параметра значение, которое он будет принимать,
если пользователь пропустит его или передаст undefined. Та кие параметры называются
параметрами со значением по умолчанию или просто параметрами по умолчанию. Возьмем
предыдущий пример и зададим для lastName значение по умолчанию, равное "Smith".
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob");
let result2 = buildName("Bob", undefined);
// пока что все правильно, возвращает "Bob Sm
// тоже работает и возвращает "Bob Sm
let result3 = buildName("Bob", "Adams", "Sr."); // ошибка, слишком много параметров
let result4 = buildName("Bob", "Adams"); // в самый раз
Параметры по умолчанию, которые следуют после всех обязательных параметров,
считаются опциональными. Так же, как и опциональные, их можно пропускать при вызове
функции. Это означает, что типы опциональных параметров и параметров по умолчанию,
которые находятся в конце, будут совместимы, так что эта функция:
function buildName(firstName: string, lastName?: string) {
// ...
}
и эта
function buildName(firstName: string, lastName = "Smith") {
// ...
}
будут иметь одинаковый тип (firstName: string, lastName?: string) => string.
Значение по умолчанию для параметра lastName в описании типа функции исчезает, и
остается лишь тот факт, что последний параметр необязателен.
В отличие от простых опциональных параметров, параметры по умолчанию не обязательно
должны находиться после обязательных параметров. Если после параметра по умолчанию
будет идти обязательный, то придется явно передать undefined, чтобы задать значение по
умолчанию. К примеру, последний пример можно переписать, используя для firstName только
параметр по умолчанию:
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob");
let result2 = buildName("Bob", "Adams", "Sr."); // ошибка, слишком много параметров
let result3 = buildName("Bob", "Adams"); // подходит, возвратит "Bob Adams"
// ошибка, слишком мало параметров
let result4 = buildName(undefined, "Adams"); // подходит, возвратит "Will Adams"
Rest parameters
Обязательные, опциональные и параметры по умолчанию имеют одну общую для всех черту
— они описывают по одному параметру за раз. В некоторых случаях нужно работать с
несколькими параметрами, рассматривая их как группу; а иногда заранее неизвестно, сколько
параметров функция будет принимать. В JavaScript с аргументами можно работать
напрямую, используя переменную arguments, которая доступна внутри любой функции.
В TypeScript можно собрать аргументы в одну переменную:
function buildName(firstName: string, ...restNames: string[]) {
return firstName + " " + restNames.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
Остаточные параметры (rest parameters) можно понимать как неограниченное число
необязательных параметров. При передаче аргументов для остаточных параметров их можно
передать столько, сколько угодно; а можно и вообще ничего не передавать. Компилятор
строит массив из переданных аргументов, присвоит ему имя, которое указано после
многоточия (...), и сделает его доступным внутри функции.
Многоточие используется и при описании типа функции с остаточными параметрами:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
Object Oriented Programming
ООП - это методология программирования, основанная на представлении программы в виде
совокупности объектов, каждый из которых является экземпляром определенного класса, а
классы образуют иерархию наследования[1].
Идеологически ООП — подход к программированию как к моделированию
информационных объектов, решающий на новом уровне основную задачу структурного
программирования: структурирование информации с точки зрения управляемости[2], что
существенно улучшает управляемость самим процессом моделирования, что, в свою очередь,
особенно важно при реализации крупных проектов.
Управляемость для иерархических систем предполагает минимизацию избыточности
данных (аналогичную нормализации) и их целостность, поэтому созданное удобно
управляемым — будет и удобно пониматься. Та ким образом, через тактическую задачу
управляемости решается стратегическая задача — транслировать понимание задачи
программистом в наиболее удобную для дальнейшего использования форму.
Основные принципы структурирования в случае ООП связаны с различными аспектами
базового понимания предметной задачи, которое требуется для оптимального управления
соответствующей моделью:
абстракция для выделения в моделируемом предмете важного для решения конкретной
задачи по предмету, в конечном счёте — контекстное понимание предмета,
формализуемое в виде класса;
• инкапсуляция для быстрой и безопасной организации собственно иерархической
управляемости: чтобы было достаточно простой команды «что делать», без
одновременного уточнения как именно делать, так как это уже другой уровень
управления;
• наследование для быстрой и безопасной организации родственных понятий: чтобы
было достаточно на каждом иерархическом шаге учитывать только изменения, не
дублируя всё остальное, учтенное на предыдущих шагах;
• полиморфизм для определения точки, в которой единое управление лучше
распараллелить или наоборот — собрать воедино.
Encapsulation
Одним из фундаментальных принципов объектно-ориентированного программирования
является инкапсуляция — способность определить данные, а также набор функций. которые
могут работать с этими данными, в один компонент. Большинство языков программирования
имеют концепцию классов для этой цели.
Для начала рассмотрим простейшее определение класса TypeScript:
1\. class MyClass {
2\. add(x, y) {
3\. return x + y;
4\. }
5\. }
6\.
7\. let classInstance = new MyClass();
8\. let result = classInstance.add(1,2);
9\. console.log(add(1,2) returns ${result});
Этот код достаточно прост для чтения и понимания. Мы создали класс, называли его
MyClass, с простым методом add. Чтобы использовать этот класс, мы просто создаем его
экземпляр и вызываем метод add с двумя аргументами.
Hidden and public class members
Еще одним объектно-ориентированным принципом. который используется в инкапсуляции,
является концепция скрытых данных — способность иметь общедоступные или частные
переменные. Частные переменные предназначены для скрытия от пользователя внутренней
кухни класса, то есть используются только для внутреннего пользования. Непреднамеренное
изменение этих переменных может привести к ошибкам во время выполнения.
В TypeScript есть возможность строго обозначить доступ к членам класса с помощью
ключевых слов public и private. Пытаясь получить доступ к приватному члену обозначенному
как private будет выдана ошибка времени компиляции. В качестве примера давайте посмотрим
на следующий код:
1\. class CountClass {
2\. private \_count: number;
3\. constructor() {
4\.
5\.
this.\_count = 0;
}
6\. countUp() {
7\. this.\_count ++;
8\.
}
9\. getCount() {
10\. return this.\_count;
11\.
}
12\. }
13\. let countInstance = new CountClass() ;
14\. countInstance.\_count = 17;
15\. console.log("countUp : " + test.getCountUp());
Тут \_count имеет модификатор private, что не позволит изменить его за пределами класса на
прямую. Запустив этот код мы получим ошибку:
hello.ts(39,15): error TS2341: Property '\_count' is private and only
accessible within class 'CountClass'.
Такая возможность позволяет избежать ошибок еще на этапе компиляции, что не может не
радовать.
Inheritance
В TypeScript используются привычные подходы объектно-ориентированного
программирования. Конечно, одним из самых фундаментальных подходов в области
программирования на основе классов является создание новых классов с помощью
наследования.
Давайте посмотрим на пример:
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(${this.name} moved ${distanceInMeters}m.);
}
}
class Snake extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino")
sam.move();
tom.move(34);
Этот пример показывает многие возможности наследования TypeScript, такие же, как и в
других языках. Здесь мы видим ключевое слово extends, используемое для создания
подкласса. Классы Horse и Snake основаны на классе Animal и они получают доступ к его
возможностям.
В примере показано, как переопределить методы базового класса с помощью методов,
которые указаны в подклассе. Классы Snake и Horse создают метод move, который
переопределяет метод move из класса Animal, придавая ему функциональность, специфичную
для каждого из классов. Обратите внимание на то, что хотя tom объявлен как Animal, его
значением является Horse, поэтому при вызове tom.move(34), будет вызван переопределенный
метод класса Horse.
Производные классы, содержащие функции-конструкторы, должны вызывать super(),
который будет выполнять функцию-конструктор базового класса.
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.
Polymorphism
Полиморфизм в объектно-ориентированном программировании – это возможность
обработки разных типов данных, т. е. принадлежащих к разным классам, с помощью "одной и
той же" функции, или метода. На самом деле одинаковым является только имя метода, его
исходный код зависит от класса.
Все то, что во время компиляции или выполнения программы может содержать или
обрабатывать значения различных типов — является полиморфным, например:
переменные, меняющие свое значение на значение другого типа;
• объекты, обладающие свойствами, которые могут менять значение текущего типа на
значение другого типа;
• функции, принимающие аргументы различных типов.
В TypeScript есть поддержка дженериков, поэтому мы можем описать функцию identity,
которая будет работать со всеми типами. Для этого воспользуемся переменными типов:
1\. class A {
2\. prop: number
3\. constructor(prop: number) {
4\. this.prop = prop
5\. }
6\. }
7\.
8\. class AA extends A {}class B {
9\. prop: number
10\. constructor(prop: number) {
11\. this.prop = prop
12\. }
13\. }
14\.
15\. class BB extends B {}function output(objects: A[]) {
16\. objects.forEach((object: A) => {
17\. console.log(obj.prop)
18\. })
19\. }
20\.
21\. // 1const a:A = new A(1)
22\. const b:B = new B
23\. (2)output([a, a, a, a])
24\. output([a, a, b, b])
25\.
26\. // 2const aa:AA = new AA(1)
27\. const bb:BB = new BB(2)output([aa, aa, aa, aa])
28\. output([aa, aa, bb, bb])
Classes
TypeScript реализует объектно-ориентированный подход, в нем есть полноценная
поддержка классов и интерфейсов. Класс представляет шаблон для создания объектов и
инкапсулирует функциональность, которую должен иметь объект. Класс определяет
состояние и поведение, которыми обладает объект.
Для определения нового класса применяется ключевое слово class. Например, определим класс
User:
class User {}
После определения класса мы можем создавать его экземпляры:
let tom: User = new User();
let alice = new User();
Здесь определено два объекта класса User - tom и alice.
Class fields
Для хранения состояния объекта в классе определяются поля:
class User {
name: string;
age: number;
}
Здесь определены два поля - name и age, которые имеют типы string и number
соответственно. Фактически поля представляют переменные уровня класса, только для их
объявления не применяются var и let.
По имени объекта мы можем обращаться к этим полям:
class User {
name: string;
age: number;
}
let tom = new User();
tom.name = "Tom";
tom.age = 36;
console.log(name: ${tom.name} age: ${tom.age}); // name: Tom age: 36
При определении полей им можно задать некоторые начальные значения:
class User {
name: string = "Tom Smith";
age: number = 18;
}
let user = new User();
console.log(name: ${user.name} age: ${user.age}); // name: Tom Smith age: 18
Methods
Классы также могут определять поведение - некоторые действия, которые должны
выполнять объекты этого класса. Для этого внутри класса определяются функции, которые
называются методами.
class User {
name: string;
age: number;
print(){
console.log(name: ${this.name} age: ${this.age});
}
toString(): string{
return ${this.name}: ${this.age};
}
}
Здесь в классе User определены два метода. Метод print() выводит информацию об объекте
на консоль, а метод toString() возвращает некоторое представление объекта в виде строки. В
отличие от обычных функций для определения методов не указывается ключевое слово
function.
Для обращения внутри методов к полям и другим методам класса применяется ключевое
слово this, которое указывает на текущий объект этого класса.
Применение методов:
class User {
name: string;
age: number;
print(){
console.log(name: ${this.name} age: ${this.age});
}
toString(): string{
return ${this.name}: ${this.age};
}
}
let tom = new User();
tom.name = "Tom";
tom.age = 36;
tom.print();
// name: Tom age: 36
console.log(tom.toString()); // Tom: 36
Constructors
Кроме обычных методов классы имеют специальные функции - конструкторы, которые
определяются с помощью ключевого слова constructor. Конструкторы выполняют начальную
инициализацию объекта.
Например, добавим в класс User конструктор:
class User {
name: string;
age: number;
constructor(userName: string, userAge: number) {
this.name = userName;
this.age = userAge;
}
print(){
console.log(name: ${this.name} age: ${this.age});
}
}
let tom = new User("Tom", 36);
tom.print(); // name: Tom age: 36
Здесь конструктор принимает два параметра и использует их значения для установки
значения полей name и age:
constructor(userName: string, userAge: number) {
this.name = userName;
this.age = userAge;
}
Затем при создании объекта в конструктор передается два значения для его параметров:
let tom = new User("Tom", 36);
Access Modifiers
public
В наших примерах мы смогли свободно получить доступ к членам класса, объявленным во
всех классах программы. Если вы знакомы с классами в других языках, вы могли заметить,
что в приведенных выше примерах мы не использовали слово public для изменения видимости
члена класса. Например, C# требует, чтобы каждый член был явно помечен public для
видимости. В TypeScript же, каждый член класса будет public по умолчанию.
Но мы можем пометить члены класса public явно.
Класс Animal из предыдущего раздела будет выглядеть следующим образом:
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(${this.name} moved ${distanceInMeters}m.);
}
}
private
Когда член класса помечен модификатором private, он не может быть доступен вне этого
класса.
Например:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // ошибка: 'name' is private;
TypeScript — это структурная система типов. Когда мы сравниваем два разных типа,
независимо от того где и как они описаны и реализованы, если типы всех их членов
совместимы, можно утверждать, что и сами типы совместимы. Впрочем, когда сравниваются
типы с модификатором доступа private, это происходит по-другому. Два типа будут считаться
совместимыми, если оба члена имеют модификатор private из того же самого объявления.
Это относится и к protected членам.
Давайте посмотрим пример, чтобы понять принцип работы на практике:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
animal = rhino;
animal = employee; // ошибка: 'Animal' and 'Employee' are not compatible
В этом примере у нас есть классы Animal и Rhino, где Rhino является подклассом Animal.
У нас также есть новый класс Employee, который выглядит идентично Animal. Мы создаем
экземпляры этих классов и пытаемся получить доступ к каждому, чтобы посмотреть что
произойдет. Поскольку private часть Animal и Rhino объявлена в одном и том же объявлении,
они совместимы. Тем не менее, это не относится к Employee. Когда мы пытаемся присвоить
Employee к Animal, мы получаем ошибку: эти типы не совместимы. Несмотря на то, что
Employee имеет private член под именем name, это не тот член, который мы объявили в
Animal.
protected
Модификатор protected действует аналогично private за исключением того, что члены,
объявленные protected, могут быть доступны в подклассах. Например:
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return Hello, my name is ${this.name} and I work in ${this.department}.;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // ошибка
Обратите внимание на то, что мы не можем использовать член name вне класса Person, но
можем использовать внутри метода подкласса Employee, потому что Employee происходит от
Person.
Конструктор тоже может иметь модификатор protected. Это означает, что класс не может
быть создан за пределами содержащего его класса, но может быть наследован. Например:
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// Employee can extend Person
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return Hello, my name is ${this.name} and I work in ${this.department}.;
}
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // ошибка: The 'Person' constructor is protected
readonly
Вы можете делать свойства доступными только для чтения с помощью ключевого слова
readonly. Свойства, доступные только для чтения, должны быть инициализированы при их
объявлении или в конструкторе.
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // ошибка! name is readonly.
Parameter Properties
В нашем последнем примере мы объявили readonly член name и параметр конструктора
theName в классе Octopus, и присвоили theName к name. Это очень распространенная
практика. свойства параметров позволяют создавать и инициализировать члены в одном
месте.
Вот дальнейшая доработка предыдущего класса Octopus, используя свойство параметра:
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
Обратите внимание на то, как мы убрали theName и сократили параметр конструктора
readonly name: string, чтобы создать и инициализировать член name. Мы объединили
объявление и присваивание в одном месте.
Свойства параметров объявляются перед параметром конструктора, у которого есть
модификатор доступности, readonly или и то, и другое. Использование свойства параметра
private объявляет и инициализирует приватный член; то же самое делают public,protected и
readonly.
Accessors
TypeScript поддерживает геттеры и сеттеры как способ перехвата обращений к свойствам
объекта. Это дает вам больший контроль над моментом взаимодействия со свойствами
объектов.
Давайте перепишем простой класс с использованием get и set. Для начала запишем пример
без использования геттеров и сеттеров.
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
Разрешать напрямую устанавливать fullName - довольно удобно, но это может привести к
проблемам если кто-то захочет изменить имя по своему желанию.
В этой версии мы проверяем наличие у пользователя секретного пароля, перед тем как
позволить ему внести изменения. Мы делаем это заменяя прямой доступ к fullName и
используем сеттер set, который проверяет пароль. Кроме того, добавляем соответствующий
get, чтобы код работал так же, как и в предыдущем примере.
let passcode = "secret passcode";
class Employee {
private \_fullName: string;
get fullName(): string {
return this.\_fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
this.\_fullName = newName;
}
else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
Чтобы убедиться, что наш метод доступа проверяет пароль, мы можем модифицировать
его и увидеть, что при несовпадении мы получаем сообщение о том, что не можем
модифицировать объект работника.
Abstract classes
Абстрактные классы представляют классы, определенные с ключевым словом abstract. Они
во многом похожи на обычные классы за тем исключением, что мы не можем создать
напрямую объект абстрактного класса, используя его конструктор.
abstract class Figure {
}
// let someFigure = new Figure() // Ошибка!
Как правило, абстрактные классы описывают сущности, которые в реальности не имеют
конкретного воплощения. Например, геометрическая фигура может представлять круг,
квадрат, треугольник, но как таковой геометрической фигуры самой по себе не существует.
Есть конкретные фигуры, с которыми мы и работаем. В то же время все фигуры могут иметь
какой-то общий функционал. В этом случае мы можем определить абстрактный класс фигуры,
поместить в него общий функционал, и от него наследовать классы конкретных
геометрических фигур:
abstract class Figure {
getArea(): void{
console.log("Not Implemented")
}
}
class Rectangle extends Figure{
constructor(public width: number, public height: number){
super();
}
getArea(): void{
let square = this.width \* this.height;
console.log("area =", square);
}
}
let someFigure: Figure = new Rectangle(20, 30)
someFigure.getArea(); // area = 600
В данном случае абстрактный класс определяет метод getArea(), который вычисляет
площадь фигуры. Класс прямоугольника определяет свою реализацию для этого метода.
Abstract methods
Однако в данном случае метод getArea в базовом классе не выполняет никакой полезной
работы, так как у абстрактной фигуры не может быть площади. И в этом случае подобный
метод лучше определить как абстрактный:
abstract class Figure {
abstract getArea(): void;
}
class Rectangle extends Figure{
constructor(public width: number, public height: number){
super();
}
getArea(): void{
let square = this.width \* this.height;
console.log("area =", square);
}
}
let someFigure: Figure = new Rectangle(20, 30)
someFigure.getArea();
Абстрактный метод не определяет никакой реализации. Если класс содержит абстрактные
методы, то такой класс должен быть абстрактным. Кроме того, при наследовании
производные классы обязаны реализовать все абстрактные методы.
Abstract fields
Та кже абстрактный метод может иметь абстрактные поля, то есть поля определенные с
модификатором abstract. При наследовании класс-наследник также обязан предоставить для
них реализацию:
abstract class Figure {
abstract x: number;
abstract y: number;
abstract getArea(): void;
}
class Rectangle extends Figure{
//x: number;
//y: number;
constructor(public x: number, public y: number, public width: number, public height: numb
super();
}
getArea(): void{
let square = this.width \* this.height;
console.log("area =", square);
}
}
let someFigure: Figure = new Rectangle(10, 10, 20, 25)
someFigure.getArea();
В данном случае класс Figure определяет два абстрактных поля x и y, которые условно
представляют начальную точку фигуры:
abstract x: number;
abstract y: number;
Класс Rectangle предоставляет для них реализацию с помощью определения полей через
параметры конструктора:
constructor(public x: number, public y: number, public width: number, public height: number)
Interfaces
Одним из основных принципов TypeScript является то, что проверка типов основывается на
форме значений. Этот подход иногда называется "утиной типизацией" - Если это выглядит
как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка. В TypeScript
интерфейсы выполняют функцию именования типов, и являются мощным способом
определения соглашений внутри кода, а также за пределами проекта.
Interface Implementation
В таких языках, как C# и Java интерфейсы наиболее часто используются для того, чтобы
явно указать, что класс соответствует определенному соглашению. Это возможно и в
TypeScript.
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
Та кже в интерфейсе можно описать методы, которые реализованы внутри класса, как это
сделано для setTime в следующем примере:
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
Интерфейсы описывают публичную часть класса, но не приватную. Это не дает
возможности указывать с помощью интерфейса, что класс должен использовать конкретные
типы для своих приватных членов.
Difference b etween static part and class instance
Работая с классами и интерфейсами, полезно помнить, что класс имеет два типа: тип
статической части и тип экземпляра. Вы могли столкнуться с ошибкой, если создавали
интерфейс с конструктором, а потом пытались написать класс, который реализовывал бы его:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
Так происходит из-за того, что, когда класс реализует интерфейс, происходит проверка
типа только его экземпляра. Конструктор же находится в статической части, и не включается
в эту проверку.
Вместо такого подхода нужно работать напрямую со статической частью класса. В
следующем примере мы определяем два интерфейса: ClockConstructor для конструктора, и
ClockInterface для экземпляра класса. Затем, для удобства, мы определяем функцию-
конструктор createClock, которая создает объекты того типа, который передается ей в
качестве аргумента.
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterfac
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
Так как первый параметр createClock имеет тип ClockConstructor, то в
createClock(AnalogClock, 7, 32) происходит проверка на то, что AnalogClock имеет
подходяющую сигнатуру конструктора.
Interface Extends
Интерфейсы могут расширять друг друга, подобно классам. Это позволяет копировать
члены одного интерфейса в другой, что дает больше гибкости при разделении интерфейсов на
переиспользуемые компоненты.
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <square>{};
square.color = "blue";
square.sideLength = 10;
</square>
Интерфейс может расширять сразу несколько других интерфейсов, создавая их комбинацию:
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
</square>
Interfaces Extending Classes
Когда интерфейс расширяет класс, интерфейс наследует члены класса, но не их
реализацию. Это аналогично тому, как если бы интерфейс описывал все члены класса, но не
указывал их реализацию. Интерфейсы наследуют даже приватные и защищенные члены
базового класса. Это означает, что если создать интерфейс, расширяющий класс с
приватными или защищенными членами, то он может быть реализован только самим базовым
классом либо его наследниками.
Это полезно в тех случаях, когда существует большая иерархия наследования, и нужно
указать, что код работает только с определенными подклассами, у которых есть
определенные свойства. Та кие подклассы не обязаны иметь отношение друг к другу, кроме
того, что они наследуются от одного и того же базового класса.
К примеру:
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control {
select() { }
}
class TextBox extends Control {
select() { }
}
class Image extends Control {
}
class Location {
select() { }
}
В этом примере SelectableControl содержит все члены класса Control, включая приватное
свойство state. Так как state — приватный член, реализовать интерфейс SelectableControl
смогут только наследники Control. Так будет потому, что для совместимости приватных
членов необходимо, чтобы они были объявлены в одном и том же базовом классе, а это
возможно лишь для наследников Control.
Внутри кода Control можно получить доступ к приватному члену state через экземпляр
SelectableControl. По сути, SelectableControl ведет себя так же, как Control, о котором
известно, что у него есть метод select. Классы Button и TextBox — подтипы SelectableControl
(так как оба унаследовано от Control и у них есть метод select), однако Image и Location
таковыми не являются.
Asynchronous programming
Традиционно в программировании используют синхронное программирование —
последовательное выполнение инструкций с синхронными системными вызовами, которые
полностью блокируют поток выполнения, пока системная операция, например чтение с диска,
не завершится.
Асинхронность в программировании — выполнение процесса в не блокирующем режиме
системного вызова, что позволяет потоку программы продолжить обработку. Реализовать
асинхронное программирование можно несколькими способами, о которых вы узнаете ниже.
Оператор async определяет асинхронную функцию, в которой, как предполагается, будет
выполняться одна или несколько асинхронных задач:
async function название\_функции(){
// асинхронные операции
}
Внутри асинхронной функции мы можем применить оператор await. Он ставится перед
вызовом асинхронной операции, которая представляет объект Promise:
async function название\_функции(){
await асинхронная\_операция();
}
Оператор await приостанавливает выполнение асинхронной функции, пока объект Promise
не возвратить результат. Стоит учитывать, что оператор await может использоваться только
внутри функции, к которой применяется оператор async.
Сначала рассмотрим самый простейший пример с использованием Promise:
function sum(x, y){
return new Promise(function(resolve){
const result = x + y;
resolve(result);
});
}
sum(5, 3).then(function(value){
console.log("Результат асинхронной операции:", value);
}); // Результат асинхронной операции: 8
В данной случае функция sum() представляет асинхронную задачу. Она принимает два
числа и возвращает объект Promise, в котором выполняется сложение этих чисел. Результат
сложения передается в функцию resolve(). И далее в методе then() мы можем получить этот
результат и выполнить с ним различные действия.
Теперь перепишем этот пример с использованием async/await:
function sum(x, y){
return new Promise(function(resolve){
const result = x + y;
resolve(result);
});
}
async function calculate(){
const value = await sum(5, 3);
console.log("Результат асинхронной операции:", value);
}
calculate(); // Результат асинхронной операции: 8
Здесь мы определили асинхронную функцию calculate(), к которой применяется async:
async function calculate(){
Внутри функции вызывается асинхронная операция sum(), которой передаются некоторые
значения. Причем к этой функции применяется оператор await. Благодаря оператору await
больше нет надобности вызывать у промеса метод then(). А результат, который возвращает
Promise, мы можем получить напрямую из вызова функции sum и, например, присвоить
константе или переменной:
const value = await sum(5, 3);
Затем мы можем вызвать функцию calculate() как обычную функции и тем самым выполнить
все ее действия.
calculate();
Async и await позволяет писать асинхронный код так, что он выглядит и действует как
синхронный. Такой код становится гораздо проще читать, писать и судить о нем.
Await работает только внутри функции async
• Функция, помеченная ключевым словом async, всегда возвращает Promise
• Если возвращаемое значение внутри async не возвращает Promise, то оно будет
обернуто в немедленно разрешаемый Promise
• Как только встретится ключевое слово await, выполнение приостанавливается, пока
не будет завершено выполнение Promise
• Await либо вернет результат от выполненного Promise, либо выбросит исключение от
отклоненного Promise
Sequence of asynchronous operations
Асинхронная функция может содержать множество асинхронных операций, к которым
применяется оператор await. В этом случае все асинхронные операции будут выполняться
последовательно:
1\. function sum(x, y){
2\. return new Promise(function(resolve){
3\.
const result = x + y;
4\.
resolve(result);
5\. });
6\. }
7\.
8\. async function calculate(){
9\. const value1 = await sum(5, 3);
10\. console.log("Результат 1 асинхронной операции:", value1);
11\. const value2 = await sum(6, 4);
12\. console.log("Результат 2 асинхронной операции:", value2);
13\. const value3 = await sum(7, 5);
14\. console.log("Результат 3 асинхронной операции:", value3);
15\. }
16\. calculate();
17\. // Результат 1 асинхронной операции: 8
18\. // Результат 2 асинхронной операции: 10
19\. // Результат 3 асинхронной операции: 12
Promises
В сущности, async/await – это синтаксический сахар для промисов, то есть, ключевое слово
async/await обертывает промисы. Функция async всегда возвращает промис. Даже если
пропустить ключевое слово Promise, компилятор обернет вашу функцию в немедленно
разрешаемый промис.
1\. const myAsynFunction = async (url: string): Promise<T> => {
2\. const { data } = await fetch(url)
3\. return data
4\. }
5\.
6\. const immediatelyResolvedPromise = (url: string) => {
7\. const resultPromise = new Promise((resolve, reject) => {
8\.
resolve(fetch(url))
9\. })
10\. return resultPromise
11\. }
What is a promise in TypeScript?
В переводе с английского «promise» означает «обещание». В JavaScript промис описывает
ожидание того, что некоторое событие произойдет в определенный момент, и ваше
приложение полагается на результат этого будущего события при выполнении определенных
других задач.
Sequential execution with .then
При программировании клиентского интерфейса есть типичная задача: выполнять запросы
по сети и адекватно реагировать на их результаты.
Ниже – запрос, требующий выбрать список сотрудников с удаленного сервера.
1\. const api = 'http://dummy.restapiexample.com/api/v1/employees'
2\. fetch(api)
3\. .then(response => response.json())
4\. .then(employees => employees.forEach(employee => console.log(employee.id)) // логир
5\. .catch(error => console.log(error.message))) // логирует любую ошибку, приходящую о
Error handling in try/catch
Вернемся к примеру с выбором записей о сотрудниках, чтобы показать обработку ошибок в
действии, поскольку именно при выполнении запроса по сети ошибка вполне может
возникнуть.
Допустим, например, что у нас лег сервер, либо что мы отправили запрос в неверном
формате. Мы должны приостановить выполнение, чтобы предотвратить обвал программы.
Синтаксис будет выглядеть так:
1\. interface Employee {
2\. id: number
3\. employee\_name: string
4\. employee\_salary: number
5\. employee\_age: number
6\. profile\_image: string
7\. }
8\. const fetchEmployees = async (): Promise<Array<Employee> | string> => {
9\. const api = 'http://dummy.restapiexample.com/api/v1/employees'
10\. try {
11\.
12\.
13\.
const response = await fetch(api)
const { data } = await response.json()
return data
14\. } catch (error) {
15\.
if (error) {
16\.
return error.message
17\.
}
18\.
}
19\. }
Мы инициировали функцию async. В качестве возвращаемого значения ожидаем массив
типа typeof с информацией о сотрудниках, либо строку с сообщениями об ошибке.
Соответственно, тип Promise формулируется как Promise<Array | string>.
В блоке try находятся выражения, которые функция должна выполнять, если ошибок не
будет. Блок catch захватывает любую возникающую ошибку. В таком случае мы просто
возвращаем свойство message объекта error.
Красота происходящего заключается в том, что любая ошибка, рождающаяся в блоке try,
сразу выбрасывается и захватывается блоком catch. Если какое-то исключение ускользнет, то
может получиться код, плохо поддающийся отладке, либо даже может быть испорчена вся
программа.
throw
Инструкция throw позволяет генерировать исключения, определяемые пользователем. При
этом выполнение текущей функции будет остановлено (инструкции после throw не будут
выполнены), и управление будет передано в первый блок catch в стеке вызовов. Если catch
блоков среди вызванных функций нет, выполнение программы будет остановлено.
Используйте инструкцию throw для генерирования исключения. Когда вы генерируете
исключение (throw), выражение задаёт значение исключения.
Каждое из следующих throw создаёт исключение:
1\. throw "Error2"; // генерирует исключение, значением которого является строка
2\. throw 42;
// генерирует исключение, значением которого является число 42
3\. throw true; // генерирует исключение, значением которого является логическое знач
Example: Throwing an Object as an Exception
Можно указать объект в качестве исключения. Затем можно получить ссылку на этот
объект и доступ ко всем его свойствам в блоке catch. Следующий пример создает объект
ошибки, который имеет тип UserException, и используется для генерации исключения.
1\. function UserException(message) {
2\. this.message = message;
3\. this.name = "Исключение, определённое пользователем";
4\. }
5\. function getMonthName(mo) {
6\. mo = mo-1; // Нужно скорректировать номер месяца согласно индексам массива (1=
7\. const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
8\.
9\. if (months[mo] !== undefined) {
10\. return months[mo];
11\. } else {
"Aug", "Sep", "Oct", "Nov", "Dec"];
12\.
throw new UserException("Неверно указан номер месяца");
13\. }
14\. }
15\. try {
16\. // statements to try
17\. let myMonth = 15; // 15 находится вне границ массива, что приведёт к исключению
18\. let monthName = getMonthName(myMonth);
19\. } catch (e) {
20\. monthName = "неизвестен";
21\. logMyErrors(e.message, e.name); // передаём исключение в обработчик ошибок
22\. }
Decorators for functions
Определение в самом Typescript выглядит следующим образом:
1\. declare type MethodDecorator =
2\. <T>(
3\.
4\.
5\.
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>)
6\. => TypedPropertyDescriptor<T> | void;
Это функция, принимающая несколько аргументов.
объект, у которого данная функция была вызвана
• имя функции
• дескриптор функции
Дескриптор выглядит так:
1\. interface TypedPropertyDescriptor<T> {
2\. enumerable?: boolean;
3\. configurable?: boolean;
4\. writable?: boolean;
5\. value?: T;
6\. get?: () => T;
7\. set?: (value: T) => void;
8\. }
Дескриптор нужен, чтобы получить доступ к исходной функции и иметь возможность ее
вызвать из кода декоратора. Чтобы рассмотреть пример, нам понадобится какой-нибудь
понятный и полезный сценарий. Используем — измерение производительности функции.
1\. class TestServiceDeco {
2\.
3\. @LogTime()
4\.
testLogging() {
5\.
...
6\.
}
7\. }
Декоратор для функции, свойства или параметра функции можно применить только внутри
некоего класса. В настоящее время компилятор Typescript не позволит применить декоратор
для функции, которая написана вне класса.
Для нашего сценария код декоратора может выглядеть таким образом:
1\. function LogTime() {
2\. return (target: Object, propertyName: string, descriptor: TypedPropertyDescriptor<Func
3\.
4\.
5\.
6\.
7\.
8\.
9\.
const method = descriptor.value;
descriptor.value = function(...args) {
console.time(propertyName || 'LogTime');
const result = method.apply(this, args);
console.timeEnd(propertyName || 'LogTime');
return result;
};
10\. };
11\. }
Декоратор — это функция, которая возвращает функцию определенного типа. В примере
видны аргументы этой функции — target, propertyName и дескриптор функции. Их
компилятор подставит в вызывающий код.
Дескриптор функции здесь позволяет переопределить поведение — подменить искомую
функцию на новую, которая уже следует заданной декоратором логике. Наша логика
подразумевает возможность засечь момент старта функции, и ее завершения, и вывести
разницу в консоль.
Скомпилированный Javascript код будет выглядеть следующим образом
1\. "use strict";
2\. Object.defineProperty(exports, "\_\_esModule", { value: true });
3\. function LogTime() {
4\. return (target, propertyName, descriptor) => {
5\.
6\.
7\.
8\.
9\.
10\.
11\.
const method = descriptor.value;
descriptor.value = function (...args) {
console.time(propertyName || 'LogTime');
const result = method.apply(this, args);
console.timeEnd(propertyName || 'LogTime');
return result;
};
12\. };
13\. }
14\. exports.LogTime = LogTime;
Никаких сюрпризов, все примерно как и в Typescript коде. А код вызывающий немного
интереснее:
1\. Object.defineProperty(exports, "\_\_esModule", { value: true });
2\. const log\_time\_decorator\_1 = require("../src/samples/log-time.decorator");
3\. class TestServiceDeco {
4\. testLogging() {
5\. ... }
6\. }
7\. \_\_decorate([
8\. log\_time\_decorator\_1.LogTime(),
9\. \_\_metadata("design:type", Function),
10\. \_\_metadata("design:paramtypes", []),
11\. \_\_metadata("design:returntype", void 0)
12\. ], TestServiceDeco.prototype, "testLogging", null);
Здесь видна системная функция \_\_decorate, в которую передается наш декоратор вместе с
дополнительными аргументами.
Подставленный компилятором код, вызывающий функцию \_\_decorate, будет выполнен в
процессе интерпретации кода, сразу после интерпретации класса. Но сам код нашего
декоратора будет вызываться каждый раз, когда вызывается исходная функция. Это
ключевое отличие от следующего вида декораторов.
Decorators for classes
Декоратор класса применяется к конструктору класса и позволяет изменять или заменять
определение класса.
Декоратор класса представляет функцию, которая принимает один параметр:
function classDecoratorFn(constructor: Function){ }
В качестве параметра выступает конструктор класса. Например, определим простейший
декоратор:
1\. function sealed(constructor: Function) {
2\. console.log("sealed decorator");
3\. Object.seal(constructor);
4\. Object.seal(constructor.prototype);
5\. }
6\.
7\. @sealed
8\. class User {
9\. name: string;
10\. constructor(name: string){
11\.
12\.
this.name = name;
}
13\. print():void{
14\.
console.log(this.name);
15\.
}
16\. }
Декоратор sealed с помощью функции Object.seal запрещает расширение прототипа класса
User.
Для применения декоратора используется знак @. Сам декоратор ставится перед названием
класса. То есть из-за применения декоратора мы, к примеру, не сможем добавить в класс User
новое свойство следующим образом:
Object.defineProperty(User, 'age', {
value: 17
});
Та кже декораторы могут изменять результат работы конструктора. В этом случае
определение функции декоратора немного меняется, но она также в качестве параметра
принимает конструктор класса:
1\. function logger<TFunction extends Function>(target: TFunction): TFunction{
2\.
3\. let newConstructor: Function = function(name:string){
4\.
5\.
6\.
7\.
8\.
9\.
10\.
console.log("Creating new instance");
this.name = name;
this.age = 23;
this.print = function():void{
console.log(this.name, this.age);
}
}
11\. return <TFunction>newConstructor;
12\. }
13\.
14\. @logger
15\. class User {
16\. name: string;
17\. constructor(name: string){
18\.
19\.
this.name = name;
}
20\. print():void{
21\.
console.log(this.name);
22\.
}
23\. }
24\. let tom = new User("Tom");
25\. let bob = new User("Bob");
26\. tom.print();
27\. bob.print();
В данном случае декоратор logger типизирован типом TFunction, который является
расширением типа Function, то есть функции. По сути это тип функции конструктора.
В самом декораторе передаваемый конструктор target никак не используется. Но создается
новый конструктор. Мы предполагаем, что в конструктор будет передаваться некоторый
параметр, который будет называться name. Значение этого параметра передается свойству
this.name = name;. Та кже в конструкторе устанавливается новое свойство this.age и метод
this.print(), который выводит на консоль значения обоих свойств.
Далее декоратор применяется к классу User. У этого класса определен конструктор,
который устанавливает свойство name. Однако поскольку мы переопределили конструктор, то
в реальности при создании объекта User будет устанавливаться как свойство name, так и
свойство age. И, кроме того, будет переопределять метод print.
Вывод консоли браузера
// Creating new instance Creating new instance Tom 23 Bob 23
Следует учитывать, что замена конструктора приводит к полной замене всех свойств и
методов класса.
Decorators for fields or class properties
Еще одна область применения декораторов относится к свойствам класса. Представьте,
есть класс Person с полем Age, значения которого по логике приложения должно быть между
18 и 60. Давайте сделаем данную проверку с помощью декоратора:
1\. class Person {
2\. @Age(18, 60)
3\. age: number;
4\. }
Снова обратимся к формальному определению:
1\. declare type PropertyDecorator =
2\. (target: Object, propertyKey: string | symbol) => void;
Наш декоратор для валидации выглядит следующим образом:
1\. import 'reflect-metadata';
2\.
3\. function Age(from: number, to: number) {
4\. return function (object: Object, propertyName: string) {
5\.
6\.
7\.
8\.
9\.
const metadata = {
propertyName: propertyName,
range: { from, to },
};
Reflect.defineMetadata(validationMetadata\_${propertyName}, metadata, object.cons
10\. };
11\. }
Основной логики тут нет. Мы просто сохраняем нужную нам информацию в хранилище
метаданных. Все потому, что это код, как и код декоратора класса, будет выполнен только
один раз при прочтении кода. До того, как конструктор класса был вызван.
Скомпилированный код:
1\. class Person {
2\. ...
3\. }
4\. \_\_decorate([
5\. age\_decorator\_1.Age(18, 60),
6\. \_\_metadata("design:type", Number)
7\. ], Person.prototype, "age", void 0);
Видно, что сразу после определения класса компилятор поместил свою функцию
\_\_decorate, в которую передал наш декоратор с параметрами.
Это своеобразное подтверждение того, что основная задача декораторов — сделать код
более удобным к прочтению, информативно богатым. В случае валидации — описать правила
проверок в том же месте, где и сам класс, причем в удобной форме.
Возвращаясь к валидации, ее необходимо описать отдельно:
1\. function validate<T>(object: T) {
2\. const properties = Object.getOwnPropertyNames(object);
3\. properties.forEach(propertyName => {
4\.
5\.
6\.
7\.
8\.
9\.
10\.
let metadata = Reflect.getMetadata(metaKey + propertyName, object.constructor);
if (metadata && metadata.range) {
const value = object[metadata.propertyName];
if (value < metadata.range.from || value > metadata.range.to) {
throw new Error('Validation failed');
}
}
11\. });
12\. }
В примере, мы делаем одну единственную проверку. Реальный сценарий будет несколько
сложнее.
Пример вызова:
1\. const person = new Person();
2\. person.age = 40;
3\. validate(person);
4\. // > validation passed
5\.
6\. person.age = 16;
7\. validate(person);
8\. // > validation error
NPM
Программная платформа Node.js появилась в 2009 г., и с тех пор на ней были построены
сотни тысяч приложений. Одной из причин успеха стал npm – популярный пакетный менеджер,
позволяющий JS-разработчикам быстро делиться пакетами.
npm (Node Package Manager) – дефолтный пакетный менеджер для JavaScript, работающий
на Node.js. Менеджер npm состоит из двух частей:
CLI (интерфейс командной строки) – средство для размещения и скачивания пакетов,
• онлайн-репозитории, содержащие JS пакеты.
package.json
Каждый проект в JavaScript – будь то Node.js или веб-приложение – может быть
скопирован как npm-пакет с собственным описанием и файлом package.json.
Файл генерируется командой npm init при создании JavaScript/Node.js проекта со
следующими метаданными:
name: название JS библиотеки/проекта.
• version: версия проекта.
• description: описание проекта.
• license: лицензия проекта
Project setup
Настроим проект Node, но скомпилируем и запустим проект с помощью TypeScript.
Шаг 1 — Инициализация проекта npm
Для начала создайте новую папку с именем node\_project и перейдите в этот каталог.
1\. mkdir node\_project
2\. cd node\_project
Затем инициализируйте его как проект npm:
1\. npm init
После запуска npm init вам нужно будет передать npm информацию о вашем проекте. Если
вы разрешите npm принимать значения по умолчанию, вы можете добавить флаг y, чтобы
пропустить диалоги с запросом дополнительной информации:
1\. npm init -y
Теперь пространство вашего проекта настроено, и вы можете перейти к установке
необходимых зависимостей.
Шаг 2 — Установка зависимостей
Следующий шаг после инициализации базового проекта npm — установить зависимости,
требующиеся для запуска TypeScript.
Запустите следующие команды из каталога вашего проекта для установки зависимостей:
1\. npm install -D typescript@3.3.3
2\. npm install -D tslint@5.12.1
Флаг -D — сокращенное обозначение опции: --save-dev. Более подробную информацию об
этом флаге можно найти в документации npmjs.
Запустите следующую команду:
1\. tsc --init
Она сгенерирует файл tsconfig.json с правильными комментариями.
Чтобы узнать больше о доступных опциях ключ-значение, можно использовать
официальную документацию TypeScript, где приводятся разъяснения всех опций.
Теперь вы можете настроить проверку соответствия стандартам кода TypeScript для этого
проекта. Откройте в терминале корневой каталог вашего проекта, который установлен в этом
учебном модуле как node\_project, и запустите следующую команду для генерирования файла
tslint.json:
1\. ./node\_modules/.bin/tslint --init
Откройте сгенерированный файл tslint.json и добавьте соответствующее правило no-console:
tslint.json
{
"defaultSeverity": "error",
"extends": ["tslint:recommended"],
"jsRules": {},
"rules": {
"no-console": false
},
"rulesDirectory": []
}
По умолчанию модуль проверки TypeScript предотвращает использование отладки через
команды консоли, поэтому нужно явно предписать ему отключить правило по умолчанию no-
console.
Шаг 3 — Обновление файла package.json
Сейчас вы можете запускать функции в терминале по отдельности или создать скрипт npm
для их запуска.
На этом шаге мы создадим скрипт start, который выполнит компиляцию и транспиляцию
кода TypeScript, а затем запустит полученное приложение .js.
Откройте файл package.json и обновите его соответствующим образом:
1\. package.json
2\. {
3\. "name": "node-with-ts",
4\. "version": "1.0.0",
5\. "description": "",
6\. "main": "dist/app.js",
7\. "scripts": {
8\. "start": "tsc && node dist/app.js",
9\. "test": "echo \"Error: no test specified\" && exit 1"
10\. },
11\. "author": "",
12\. "license": "ISC",
13\. "devDependencies": {
14\. "tslint": "^5.12.1",
15\. "typescript": "^3.3.3"
16\. },
17\. }
18\. }
В приведенном выше фрагменте кода мы обновили путь main и добавили команду start в
раздел scripts. Если посмотреть на команду start, вы увидите, что вначале запускается
команда tsc, а затем — команда node. При этом будет проведена компиляция, и
сгенерированный вывод будет запущен с помощью node.
Команда tsc предписывает TypeScript скомпилировать приложение и поместить
сгенерированный вывод .js в указанном каталоге outDir, как указано в файле tsconfig.json.
Шаг 4 — Запуск
Теперь TypeScript и модуль проверки настроены, и мы можем приступить к сборке модуля
Node Express Server.
Вначале создайте папку src в корневом каталоге вашего проекта:
1\. mkdir src
Затем создайте файл с именем app.ts:
1\. touch src/app.ts
На этом этапе структура каталогов должна выглядеть следующим образом:
├── node\_modules/
├── src/
├── app.ts
├── package-lock.json
├── package.json
├── tsconfig.json
├── tslint.json
Откройте файл app.ts в предпочитаемом текстовом редакторе и вставьте следующий
фрагмент кода:
1\. console.log(“Hello world”)
Запустите приложение с помощью следующей команды:
1\. npm start
NPM scripts
В package.json включено поле scripts для автоматизации сборки, например:
1\. {
2\. "scripts": {
3\. "build": "tsc",
4\. "format": "prettier --write \\/\*.ts",
5\. "format-check": "prettier --check \\/\*.ts",
6\. "lint": "eslint src/\\/\*.ts",
7\. "pack": "ncc build",
8\. "test": "jest",
9\. "all": "npm run build && npm run format && npm run lint && npm run pack && npm test
10\. }
11\. }
eslint, prettier, ncc, jest могут быть установлены глобально или локально для проекта внутри
node\_modules/.bin/.
Dependencies and devDependencies
Представляют собой словари с именами npm-библиотек (ключ) и их семантические версии
(значение). Пример из шаблона TypeScript Action:
1\. {
2\. "dependencies": {
3\. "@actions/core": "^1.2.3",
4\. "@actions/github": "^2.1.1"
5\. },
6\. "devDependencies": {
7\. "@types/jest": "^25.1.4",
8\. "@types/node": "^13.9.0",
9\. "@typescript-eslint/parser": "^2.22.0",
10\. "@zeit/ncc": "^0.21.1",
11\. "eslint": "^6.8.0",
12\. "eslint-plugin-github": "^3.4.1",
13\. "eslint-plugin-jest": "^23.8.2",
14\. "jest": "^25.1.0",
15\. "jest-circus": "^25.1.0",
16\. "js-yaml": "^3.13.1",
17\. "prettier": "^1.19.1",
18\. "ts-jest": "^25.2.1",
19\. "typescript": "^3.8.3"
20\. }
21\. }
Эти зависимости устанавливаются командной npm install с флагами --save и --save-dev. Они
предназначены соответственно для использования в продакшене и разработке.
О версионировании:
• ^: последний минорный релиз. Например, ^1.0.4 установит версию 1.3.0, если это
последний минорный релиз в серии 1 мажорного релиза.
• ~: последний патч-релиз. ~1.0.4 установит 1.0.7, если эта последняя минорная версия
в серии минорных релизов 1.0.
• для задания установки определенной версии или версии из какого-либо диапазона,
используйте оператор @npm install nest@">4.15.0 <4.16.0"
Все версии пакетов будут отображены в сгенерированном файле package-lock.json.
package-lock.json
Файл описывает версии пакетов, используемые в JavaScript-проекте. Если package.json
включает общее описание зависимостей, то package-lock.json более детальный – всё дерево
зависимостей.
package-lock.json генерируется командой npm install и читается npm CLI, чтобы обеспечить
воспроизведение окружения для проекта через npm ci.
Installing packages
npm install – команда, устанавливающая пакеты.
По умолчанию npm install со знаком ^ установит последнюю версию пакета. Скачает пакет
в папку проекта node\_modules в соответствии с конфигурацией в файле package.json, обновив
версию пакета везде, где это возможно (и, в свою очередь, обновив package-lock.json). При
необходимости установки пакета глобально можно указать флаг -g .
При добавлении флага --production установятся только нужные для работы приложения
зависимости из dependencies, не раздувая node\_modules.
npm ci
Если npm install --production оптимален для продакшена, существует ли аналогичная
команда для локальной разработки? Да, она называется npm ci.
Как и раньше, если package-lock.json еще не существует в проекте, он будет сгенерирован
при вызове npm install. npm ci обращается к Lock-файлу для загрузки точной версии пакетов.
Та ким образом, на разных машинах набор пакетов останется неизменным.
npm audit
Чтобы избежать добавления в репозитории вредоносных пакетов, организация npm.js
пришла к идее аудита экосистемы, создав модуль npm audit. Он предоставляет информацию
об уязвимостях в пакетах и о существовании версий с исправлениями.
Если исправления доступны в следующих версиях пакета, npm audit fix автоматически
обновит версии затронутых зависимостей.
Public License Terms for Electronic
Versions
Автор и правообладатель разрешает следующие виды использования данного файла,
являющегося электронным представлением Произведения, без уведомления правообладателя и
без выплаты авторского вознаграждения:
Воспроизведение Произведения (полностью или частично) на бумаге путём распечатки с
помощью принтера в одном экземпляре для удовлетворения личных бытовых или учебных
потребностей;
Копирование и распространение данного файла в электронном виде, в том числе путём
записи на физические носители и путём передачи по компьютерным сетям, с соблюдением
следующих условий:
все воспроизведённые и передаваемые любым лицам экземпляры файла являются
точными копиями исходного файла в формате PDF, при копировании не производится
никаких изъятий, сокращений, дополнений, искажений и любых других изменений,
включая и изменение формата представления файла;
распространение и передача копий другим лицам производится исключительно
бесплатно, то есть при передаче не взимается никакое вознаграждение ни в какой
форме, в том числе в форме просмотра рекламы, в форме платы за носитель или за сам
акт копирования и передачи, даже если такая плата оказывается значительно меньше
фактической стоимости или себестоимости носителя, акта копирования и т.п.
Любые другие способы распространения данного файла при отсутствии письменного
разрешения автора запрещены. В частности, запрещается: внесение каких-либо изменений в
данный файл, создание и распространение искаженных экземпляров, в том числе экземпляров,
содержащих какую-либо часть произведения;
Разрешается дарение (бесплатная передача) носителей, содержащих данный файл, запись
данного файла на носители, принадлежащие другим пользователям, распространение данного
файла через бесплатные файлообменные сети и т.п.