Курс: Создание веб-приложений с использованием Angular и React

Урок №2

JavaScript и TypeScript для frontend-разработчика

Теория — 90 минут
Практика — 60 минут

Сквозной проект курса: TaskFlow

От языка к приложениюJavaScriptсинтаксислогикасобытияTypeScriptтипыконтрактыинструментыComponentsUIstatepropsAngular / Reactархитектураэкосистема
Рисунок 1. Второй урок связывает язык программирования с компонентным frontend.
Главная идея урока. Angular и React — это как две разные сборки конструктора, но обе собираются из одних и тех же «кирпичиков»: JavaScript и TypeScript. Чем лучше вы понимаете язык, тем меньше framework кажется «магией», и тем реже вы будете гадать, почему что-то не работает.

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

Содержание

  1. 1. Зачем frontend-разработчику JavaScript и TypeScript
  2. 2. Программа, выражение и инструкция
  3. 3. let, const и область видимости
  4. 4. Примитивные типы JavaScript
  5. 5. Сравнения и строгая проверка
  6. 6. Условия и guard clauses
  7. 7. Циклы и перебор данных
  8. 8. Массивы
  9. 9. map, filter, find и reduce
  10. 10. Объекты
  11. 11. Деструктуризация
  12. 12. Spread и Rest
  13. 13. Функции и параметры
  14. 14. Arrow functions
  15. 15. Callback-функции
  16. 16. Замыкания
  17. 17. this: почему он вызывает вопросы
  18. 18. Модули ES
  19. 19. Что делает импорт
  20. 20. Promise
  21. 21. async/await
  22. 22. Обработка ошибок
  23. 23. fetch и граница HTTP
  24. 24. Что JavaScript делает в браузере
  25. 25. TypeScript: зачем нужен статический анализ
  26. 26. Основные типы TypeScript
  27. 27. Type annotations и inference
  28. 28. Arrays, objects и readonly
  29. 29. Type aliases
  30. 30. Interfaces
  31. 31. Union types
  32. 32. Literal types и discriminated unions
  33. 33. Optional properties и null
  34. 34. Function types
  35. 35. Generics
  36. 36. Generic Task API
  37. 37. any, unknown и почему они разные
  38. 38. Type assertions и осторожность
  39. 39. TypeScript и JSON из API
  40. 40. Архитектура модели Task
  41. 41. Практика: JavaScript-версия TaskFlow
  42. 42. Практика: TypeScript-модель Task
  43. 43. Практика: фильтрация и преобразование задач
  44. 44. Практика: mock API и async/await
  45. 45. Практика: обработка ошибок
  46. 46. Практика: подготовка к Angular
  47. 47. Практика: подготовка к React
  48. 48. Самостоятельная работа
  49. 49. Подсказки к самостоятельной работе
  50. 50. Контрольные вопросы
  51. 51. Мини-словарь
  52. 52. Чек-лист урока
  53. 53. Источники и материалы для продолжения
Урок 2 • Раздел 1

1. Зачем frontend-разработчику JavaScript и TypeScript

Контекст. Перед тем как собирать интерфейс (кнопки, списки, формы), нам нужен «язык общения» с браузером. Этим языком и служат JavaScript и его надстройка TypeScript.

💡 Аналогия. Представьте, что Angular и React — это готовые наборы мебели (IKEA). Сама по себе коробка ничего не делает. Чтобы собрать шкаф, вам нужна отвёртка и инструкция — это JavaScript и TypeScript. Фреймворк без языка просто не «заведётся».

Когда пользователь нажимает кнопку, мы должны выполнить функцию. Когда приходит ответ API, нужно разобрать данные. Когда задача меняет статус, необходимо обновить состояние интерфейса. Всё это выражается средствами JavaScript (мы увидим это во второй половине урока).

Angular и React не скрывают JavaScript. Напротив, они предполагают, что разработчик уверенно владеет функциями, объектами, массивами, модулями и асинхронным кодом.

Цель урока. После занятия вы должны читать типичный TypeScript-код Angular/React и понимать, какую задачу решает каждая конструкция — как если бы читали рецепт и понимали, зачем каждый шаг.
Урок 2 • Раздел 2

2. Программа, выражение и инструкция

Программа состоит из команд, которые можно рассматривать как инструкции. Выражение, в свою очередь, вычисляется в значение.

Контекст. Это самый первый базовый навык: отличить «кусок кода, который что-то считает» (выражение) от «команды сделать что-то» (инструкция). В TaskFlow всё строится на этом.

💡 Аналогия. Выражение — как математический пример 10 + 20: его можно «свернуть» в ответ 30. Инструкция — как команда «запиши результат в тетрадь под именем total». Одно что-то считает, другое что-то делает.
const total = 10 + 20;
const greeting = "Привет";

console.log(total);
console.log(greeting);

Разбор по шагам:

  1. 10 + 20 — выражение, оно вычисляется в число 30.
  2. const total = ... — инструкция: создаём «ящичек» total и кладём туда 30.
  3. console.log(total) — инструкция: показываем содержимое ящичка в консоли (30).

10 + 20 — выражение: оно даёт значение 30. const total = 10 + 20; — инструкция объявления и инициализации переменной.

Что увидит пользователь: в консоли браузера (F12 → Console) появятся две строки: 30 и Привет.

Это различие пригодится позже, когда JSX, Angular template expressions и callback-функции начнут смешивать вычисления и управление потоком.

Урок 2 • Раздел 3

3. let, const и область видимости

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

Контекст. Переменная — это фундамент всего кода. В TaskFlow мы храним в переменных список задач, статус загрузки, выбранный фильтр.

💡 Аналогия. Переменная — как ячейка в шкафчике с табличкой-именем. const — это ячейка, которую нельзя переназначить на другой шкафчик (но вещи внутри можно поменять). let — ячейка, которую можно «переклеить» на другой шкаф.
const task = { title: "Изучить TS" };

task.title = "Изучить TypeScript"; // допустимо: меняем вещь внутри ячейки

// task = {}; // ошибка: переменную нельзя переназначить на другой объект

let count = 0;
count = count + 1; // допустимо: let разрешает переназначить

Разбор по шагам:

  1. Создаём константу task — объект с заголовком.
  2. Меняем текст внутри объекта — это разрешено.
  3. Пытаемся присвоить task новый объект — ошибка, потому что const.
  4. С переменной count (тип let) такое разрешено.

Область видимости означает, где имя доступно. let и const имеют блочную область видимости, связанную с фигурными скобками.

💡 Аналогия. Область видимости — как комната. Если вы написали имя на бумажке внутри комнаты (внутри { }), то снаружи этой бумажки не видно.
if (true) {
  const message = "Внутри блока";
  console.log(message);
}

// console.log(message); // ошибка: снаружи блока имя не видно

Что увидит пользователь: на экране консоли появится Внутри блока. Если раскомментировать последнюю строку — ошибка «message is not defined».

Практическое правило. По умолчанию начинайте с const. Используйте let, когда значение действительно должно быть переназначено.
Урок 2 • Раздел 4

4. Примитивные типы JavaScript

Основные примитивы, с которыми вы будете постоянно работать во frontend: string, number, boolean, null, undefined, symbol и bigint. На практике для TaskFlow чаще всего нужны строки, числа и логические значения. Symbol и bigint встретятся реже и будут показаны при необходимости.

Контекст. Тип данных — это «вид вещи», которую мы храним. От типа зависит, что с вещью можно делать.

💡 Аналогия. Типы — как отделы в почтовом отделении: письмо (string), посылка с весом (number), и галочка «да/нет» (boolean). Каждый вид обрабатывается по-своему, и нельзя наклеить марку на галочку.
const title = "Изучить Angular"; // string — текст в кавычках
const count = 3;                // number — число
const completed = false;        // boolean — true или false
const emptyValue = null;        // null — «значения нет, и это подтверждено»
let notAssigned;                // undefined — «значение ещё не присвоено»

Что увидит пользователь: сами по себе эти строки ничего не выводят, но каждая переменная теперь «знает своё место» в программе.

JavaScript динамически типизирован во время выполнения: одна переменная может получить значение другого типа. TypeScript помогает обнаруживать часть таких проблем ещё до запуска.

Урок 2 • Раздел 5

5. Сравнения и строгая проверка

Оператор === проверяет строгое равенство без неявного приведения типов между разными типами. В современном коде frontend именно он обычно предпочтителен.

Контекст. В TaskFlow мы сравниваем, например, task.completed === true, чтобы понять, выполнена ли задача.

💡 Аналогия. === — строгий контролёр на входе: «ваш билет должен быть именно таким». А == — добродушный, но рассеянный: может пропустить человека с билетом другого цвета, решив, что «в принципе подходит». Отсюда неожиданности.
5 === 5       // true  — одинаковые числа
5 === "5"     // false — число против текста: типы разные

5 == "5"      // true  — нестрогое сравнение «простило» разницу типов
5 === "5"     // false

Что увидит пользователь: в консоли true, false, true, false — именно в таком порядке.

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

Помните. null == undefined возвращает true, но null === undefinedfalse. Многие стилистические правила (например, рекомендуемые правила ESLint) запрещают использование == полностью.
Урок 2 • Раздел 6

6. Условия и guard clauses

if позволяет выполнить код только при выполнении условия. Guard clause — ранняя проверка, которая сразу завершает функцию при невозможном или нежелательном состоянии.

Контекст. В TaskFlow функция «отметить задачу выполненной» сначала проверяет: а существует ли задача вообще?

💡 Аналогия. Guard clause — как охранник у входа. Если пропуска нет — он сразу говорит «нельзя» и не пускает дальше, чтобы не тратить время на лишние проверки внутри.
function completeTask(task) {
  if (!task) {
    return; // охранник: задачи нет — выходим сразу
  }

  if (task.completed) {
    return; // уже выполнена — выходим, менять нечего
  }

  task.completed = true; // только здесь — основная работа
}

Разбор по шагам:

  1. Заходим в функцию с задачей.
  2. Если задачи нет — уходим (guard clause).
  3. Если задача уже выполнена — уходим.
  4. Только если обе проверки пройдены — ставим completed = true.

Guard clauses уменьшают вложенность. В больших компонентах это особенно удобно: сначала обрабатываются ошибочные случаи, затем основной сценарий.

Урок 2 • Раздел 7

7. Циклы и перебор данных

Классический цикл for полезен, когда нужен контроль над индексом или количеством шагов. Но в frontend часто важнее выразить операцию над коллекцией через методы массива.

Контекст. В TaskFlow нужно перебрать все задачи, чтобы, например, показать их списком на экране.

💡 Аналогия. Цикл for — как пересчитать ступеньки по номерам: «ступенька 0, ступенька 1, ступенька 2…». А for...of — как просто обойти все вещи на полке по очереди, не думая о номерах.
// предположим, что tasks — массив задач из раздела 8
for (let i = 0; i < tasks.length; i++) {
  console.log(tasks[i].title); // выводим заголовок по индексу i
}

for (const task of tasks) {
  console.log(task.title); // перебираем сами объекты задач
}

Что увидит пользователь: в консоли напечатаются заголовки всех задач (оба цикла дадут один и тот же результат, просто разными способами).

На следующем этапе мы увидим map, filter и find. Они лучше описывают намерение программы.

Урок 2 • Раздел 8

8. Массивы

Массив хранит упорядоченную коллекцию значений. В TaskFlow это список задач.

Контекст. Массив — главная «таблица данных» нашего приложения: в нём лежат все задачи по порядку.

💡 Аналогия. Массив — как вагон поезда: места пронумерованы с 0, и в каждом сидит своя задача. tasks[0] — самый первый пассажир.
const tasks = [
  { id: 1, title: "Изучить HTTP", completed: true },
  { id: 2, title: "Изучить Angular", completed: false },
  { id: 3, title: "Изучить React", completed: false }
];

console.log(tasks.length); // сколько всего задач (3)
console.log(tasks[0]);     // первая задача (с id: 1)
tasks.push({ id: 4, title: "Git", completed: false }); // добавили задачу в конец

Разбор по шагам:

  1. Создаём массив из трёх задач.
  2. tasks.length — число элементов (3).
  3. tasks[0] — доступ к первому элементу по индексу.
  4. push — «прицепляет» новую задачу в хвост массива.

Что увидит пользователь: в консоли 3, затем объект первой задачи, а массив теперь содержит 4 задачи.

Индекс начинается с нуля. Поэтому tasks[0] — первая задача.

Урок 2 • Раздел 9

9. map, filter, find и reduce

Эти методы становятся частью ежедневного языка frontend-разработчика.

Контекст. В TaskFlow: filter оставит только активные задачи, map превратит их в заголовки, find найдёт одну нужную, reduce посчитает, сколько выполнено.

💡 Аналогия. Представьте кухонные инструменты:
  • map — как конвейер (или ксерокс): каждый элемент проходит через одну и ту же операцию и выходит преобразованным (один вход → один выход).
  • filter — как ситечко для чая: пропускает только нужное, остальное отбрасывает.
  • find — как поиск по номеру: вы называете номер (например, id), и находится ровно один элемент с этим номером.
  • reduce — как накопительный итог: значения по очереди складываются в один общий результат.
const titles = tasks.map(task => task.title);
// ["Изучить HTTP", "Изучить Angular", "Изучить React"]

const active = tasks.filter(task => !task.completed);
// [объекты с completed: false]

const selected = tasks.find(task => task.id === 2);
// { id: 2, title: "Изучить Angular", completed: false }

const completedCount = tasks.reduce(
  (count, task) => count + (task.completed ? 1 : 0),
  0
); // 1

Разбор по шагам (на примере reduce):

  1. Начинаем с count = 0 (второй аргумент reduce).
  2. Для каждой задачи: если выполнена — прибавляем 1, иначе 0.
  3. В итоге count = количество выполненных.

Что увидит пользователь: titles — массив строк, active — массив невыполненных задач, selected — одна задача, completedCount = 1.

map создаёт новую коллекцию преобразованных значений. filter оставляет подходящие элементы. find ищет первый подходящий элемент. reduce сворачивает коллекцию в одно итоговое значение.

Помните. find() возвращает undefined, если элемент не найден. Всегда проверяйте результат перед использованием.
Почему это важно для React? React-листы обычно строятся через map(). Почему это важно для Angular? Те же данные будут проходить через template control flow и computed значения.
Преобразование данных: map, filter, find tasks[] { id: 1, completed: true } { id: 2, completed: false } { id: 3, completed: false } { id: 4, completed: true } filter() completed === false оставить нужные map() task.title преобразовать Результат ["Изучить Angular"] ["Изучить React"] find() id === 2 один элемент Результат { id: 2, ... } или undefined
Рисунок 2. Массивы превращаются в новые данные через цепочку методов.
Урок 2 • Раздел 10

10. Объекты

Объект объединяет связанные значения по именованным свойствам.

Контекст. В TaskFlow каждая задача — это объект: у неё есть id, заголовок и статус.

💡 Аналогия. Объект — как карточка в библиотеке: на ней написано «id: 1, title: Изучить TypeScript, completed: false». Все свойства собраны вместе под одним именем.
const task = {
  id: 1,
  title: "Изучить TypeScript",
  completed: false
};

console.log(task.title);  // читаем свойство
task.completed = true;    // меняем свойство

Что увидит пользователь: console.log напечатает Изучить TypeScript, а после присваивания задача считается выполненной.

Объекты — основа данных frontend. API возвращает объекты, формы создают объекты, компоненты получают объекты, state хранит объекты.

Урок 2 • Раздел 11

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

Деструктуризация позволяет получить значения из объекта или массива по имени или позиции.

Контекст. Вместо того чтобы писать task.title снова и снова, мы «вытаскиваем» нужные поля сразу.

💡 Аналогия. Деструктуризация — как разобрать подарочную коробку: вы достаёте сразу яблоко, конфету и записку, вместо того чтобы каждый раз лезть в коробку за одним предметом.
const task = {
  id: 1,
  title: "Изучить TypeScript",
  completed: false
};

const { id, title, completed } = task; // достали три поля сразу

console.log(title); // можно использовать напрямую

Разбор по шагам:

  1. Берём объект task.
  2. В фигурных скобках перечисляем имена полей, которые хотим вытащить.
  3. Теперь title, id, completed — отдельные переменные.

В React вы постоянно встретите конструкцию вроде function TaskItem({ task }). Это деструктуризация параметра функции. В Angular деструктуризация используется аналогично, например при обработке данных внутри метода класса компонента: const { title, completed } = this.task;.

Урок 2 • Раздел 12

12. Spread и Rest

Оператор spread ... позволяет развернуть элементы. При работе с объектами он часто используется для создания обновлённой копии.

Контекст. В TaskFlow, когда мы меняем задачу, важно не «портить» старую, а сделать новую копию с изменением.

💡 Аналогия. Spread ... — как ксерокс: вы копируете все страницы старой папки и на одной из них делаете правку. Старая папка остаётся нетронутой.
// task = { id: 1, title: "Изучить TypeScript", completed: false }
const updatedTask = {
  ...task,          // скопировали все поля старой задачи
  completed: true   // и перезаписали одно поле
};

Что увидит пользователь: updatedTask равен { id: 1, title: "Изучить TypeScript", completed: true }, а исходный task не изменился.

Это особенно важно для state: вместо изменения исходного объекта мы создаём новый объект с нужным изменением.

Rest

💡 Аналогия. Rest — как пакет «всё остальное»: вы взяли первый подарок в руки, а всё, что осталось, сложили в один большой пакет labels.
function logTask(id, ...labels) {
  console.log(id);
  console.log(labels);
}

logTask(1, "важно", "срочно"); // labels = ["важно", "срочно"]

В rest-параметр попадают остальные аргументы.

Урок 2 • Раздел 13

13. Функции и параметры

Функция — основной способ организации поведения программы. Она получает входные значения, выполняет действия и может вернуть результат.

Контекст. В TaskFlow функция toggleTask переключает статус задачи «выполнена/не выполнена».

💡 Аналогия. Функция — как кофемашина: вы насыпаете зёрна (параметры), нажимаете кнопку (вызов), и получаете чашку кофе (результат). Сама машина при этом остаётся на месте.
function toggleTask(task) {
  return {
    ...task,
    completed: !task.completed // инвертируем: было true → стало false, и наоборот
  };
}

const next = toggleTask({
  id: 1,
  title: "Learn",
  completed: false
});

Разбор по шагам:

  1. Вызываем toggleTask с задачей.
  2. Внутри делаем копию задачи и переворачиваем completed.
  3. Возвращаем новую задачу, старую не трогая.

Что увидит пользователь: next будет { id: 1, title: "Learn", completed: true }.

Хорошая функция делает одну понятную операцию и имеет ясный контракт.

Функция: вход → логика → выход Параметры task: Task id: number options?: Config Логика функции 1. Проверка (guard clause) 2. Преобразование данных 3. Вычисление результата 4. Возврат нового значения Результат Task (новый объект) boolean (проверка) void (побочный эффект)
Рисунок. Функция получает данные, обрабатывает, возвращает результат.
Урок 2 • Раздел 14

14. Arrow functions

Arrow function — компактный синтаксис функции.

Контекст. В TaskFlow мы пишем их внутри map/filter: tasks.filter(task => !task.completed).

💡 Аналогия. Arrow function — как «короткая записка» вместо длинного официального письма. Та же функция, но записана быстрее и компактнее.
const add = (a, b) => a + b; // одно выражение: return не нужен

const titles = tasks.map(task => task.title); // достаём заголовки

const active = tasks.filter(task => !task.completed); // только активные

// стрелочная функция с несколькими инструкциями
const processTask = (task) => {
  const updated = { ...task };
  updated.completed = true;
  return updated; // здесь return обязателен
};

Разбор по шагам:

  1. Если тело — одно выражение после =>, результат возвращается автоматически.
  2. Если тело в {} — нужно писать return явно.

Arrow functions особенно часто используются в callback-операциях. Обратите внимание на отсутствие return в стрелочной функции с одним выражением и наличие фигурных скобок и явного return в функции с несколькими инструкциями.

Angular. Стрелочные функции нельзя использовать как обработчики в шаблоне ((click)="handleClick()"), потому что Angular не отслеживает изменения при передаче стрелочной функции.
Урок 2 • Раздел 15

15. Callback-функции

Callback — функция, которую передали другой функции, чтобы она была вызвана позже. Массивы, события и асинхронные операции используют этот паттерн повсеместно.

Контекст. В TaskFlow: когда пользователь нажимает «выполнить», мы передаём функцию-обработчик, которая сработает в момент клика.

💡 Аналогия. Callback — как номерок в пиццерии: вы оставляете его («позвони мне, когда пицца будет готова»), а пекарь вызывает вас позже, когда заказ испечётся.
function runOperation(operation) {
  operation(); // вызываем переданную функцию в нужный момент
}

runOperation(() => {
  console.log("Выполняем операцию");
});

Что увидит пользователь: в консоли появится Выполняем операцию — потому что runOperation вызвала переданный callback.

React event handlers и многие методы массивов используют callback-функции. Angular template также приводит нас к обработчикам событий, которые вызывают методы класса.

Урок 2 • Раздел 16

16. Замыкания

Замыкание возникает, когда функция сохраняет доступ к переменным внешней области видимости даже после завершения внешней функции.

Контекст. В TaskFlow это пригодится, чтобы «запомнить» состояние между действиями, например счётчик выполненных задач.

💡 Аналогия. Замыкание — как блокнот, который функция прячет в карман: даже когда внешняя ситуация закончилась, блокнот остаётся с ней, и она может дописывать в него новые записи.
function createCounter() {
  let count = 0; // «блокнот» в кармане

  return function () {
    count++;       // каждый вызов прибавляет 1
    return count;
  };
}

const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2

Разбор по шагам:

  1. createCounter создаёт переменную count = 0 и возвращает внутреннюю функцию.
  2. Внутренняя функция «помнит» count (замыкание).
  3. Каждый вызов counter() увеличивает и возвращает счётчик.

Что увидит пользователь: 1, затем 2.

Это полезно для понимания private-состояния, callback-функций и некоторых паттернов Hooks. Например, в React хук useState использует замыкание для хранения значения между рендерами компонента.

Урок 2 • Раздел 17

17. this: почему он вызывает вопросы

this в JavaScript зависит от контекста вызова. Для новичка важно не пытаться вывести одно универсальное правило из всех случаев. Лучше понимать разницу между обычной функцией и arrow function.

Контекст. В объекте user метод sayHello обращается к this.name — то есть к имени самого пользователя.

💡 Аналогия. this — как слово «я» в разговоре. Если вы говорите «я устал» в своей комнате — понятно, о ком речь. Но если записать ваши слова на автоответчик и проиграть в чужой компании, «я» может указывать не туда. Так this зависит от того, «где» его произнесли.
const user = {
  name: "Anna",
  sayHello() {
    console.log(this.name); // this → user, поэтому "Anna"
  }
};

user.sayHello(); // Anna

// Разница с arrow function:
const user2 = {
  name: "Bob",
  delayedHello() {
    setTimeout(function () { console.log(this.name); }, 100);
    // undefined — обычная функция потеряла контекст user2
  },
  delayedHelloArrow() {
    setTimeout(() => { console.log(this.name); }, 100);
    // "Bob" — arrow захватывает внешний this
  }
};

Что увидит пользователь: user.sayHello() напечатает Anna; в delayedHello через таймер придёт undefined, а в delayedHelloArrowBob.

В современных React function components мы часто избегаем ручной работы с this. В Angular класс компонента использует обычный контекст экземпляра, поэтому там проблема выглядит иначе.

Урок 2 • Раздел 18

18. Модули ES

Модуль позволяет разделить программу на файлы. Каждый файл может экспортировать значения и импортировать зависимости.

Контекст. В TaskFlow тип Task мы вынесем в отдельный файл и будем «подключать» его, где нужно.

💡 Аналогия. Модуль — как отдельная коробка с инструментами. Вы подписываете её («экспорт») и берёте нужный инструмент из чужой коробки («импорт»), вместо того чтобы хранить всё в одной гигантской куче.
// task.ts
export type Task = {
  id: number;
  title: string;
  completed: boolean;
};

// task-utils.ts
import type { Task } from "./task"; // берём тип Task из соседнего файла

export function toggleTask(task: Task): Task {
  return { ...task, completed: !task.completed };
}

И Angular, и React активно используют ES modules. Поэтому import/export нужно знать уверенно.

Примечание. Ключевое слово type в import type и export type — особенность TypeScript, о которой мы поговорим подробнее позже.
ES Modules: import / export task.ts export interface Task { id: number; title: string; completed: boolean; } export function toggle(t) { return { ...t, completed: !t.completed }; } app.ts import { Task, toggle } from './task'; const task: Task = { id: 1, title: 'Learn', completed: false }; Преимущества • Изоляция: каждый файл — свой мир • Повторное использование: import где угодно • Tree-shaking: сборщик убирает неиспользуемое • Автодополнение: IDE знает экспорт • Контракт: export = публичный API • Angular и React используют import/export
Рисунок 4. Модули связывают файлы через import и export.
Урок 2 • Раздел 19

19. Что делает import

import связывает текущий модуль с экспортом другого модуля. В TypeScript часто используется import type для импорта только типа.

Контекст. В TaskFlow мы пишем import type { Task } from "./types", чтобы просто «знать форму» задачи, не подключая лишний код.

💡 Аналогия. import — как взять чужой чертёж в руки, чтобы по нему собрать деталь. import type — как посмотреть только размеры на чертеже, саму деталь не забирая.
import { useState } from "react"; // значение (функция) — нужно браузеру
import type { Task } from "./types"; // только описание типа — для проверки

Разделение типов и runtime-значений помогает понимать, что действительно понадобится браузеру, а что существует только на этапе проверки типов.

Урок 2 • Раздел 20

20. Promise

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

Контекст. В TaskFlow, когда мы загружаем задачи с сервера, ответ приходит не мгновенно — Promise «обещает» его дать позже.

💡 Аналогия. Promise — как обещание с ожиданием посылки: вы получаете обещание, что результат (посылка с данными) придёт позже — либо успешно, либо с ошибкой (посылку не доставили).
const result = fetch("https://example.com"); // result — это Promise (талончик)

result
  .then(response => response.text()) // когда готово — берём текст
  .then(html => console.log(html))  // и печатаем
  .catch(error => console.error(error)); // если ошибка — ловим её

Разбор по шагам:

  1. fetch сразу возвращает Promise, не дожидаясь ответа.
  2. .then срабатывает, когда данные пришли успешно.
  3. .catch срабатывает, если что-то пошло не так.

MDN описывает Promise как основу современной асинхронной модели JavaScript; async/await строится поверх Promise.

Жизненный цикл Promise new Promise() pending... ожидание ответа fetch(), setTimeout fulfilled resolve(data) данные получены .then(callback) rejected reject(error) ошибка сети/сервера .catch(callback) async / await const data = await fetch(url) код выглядит синхронным ошибки через try/catch Пример try { const r = await fetch(...) } catch (e) { showError(e) }
Рисунок. Promise: pending → fulfilled/rejected. async/await — синтаксический сахар.
Урок 2 • Раздел 21

21. async/await

Синтаксис async/await позволяет писать асинхронный код в форме, которая напоминает последовательную программу.

Контекст. В TaskFlow загрузка задач с сервера выглядит как обычная последовательность: «подожди ответ, прочитай JSON, верни результат».

💡 Аналогия. await — как ожидание посылки по обещанию: вы «ждёте, пока придёт результат», и только когда он готов, идёте дальше. Но при этом вся остальная программа (браузер) продолжает работать.
async function loadTasks() {
  const response = await fetch("http://localhost:3000/tasks"); // ждём ответ
  const tasks = await response.json(); // ждём превращения в данные

  return tasks; // возвращаем готовый массив
}

Что увидит пользователь: функция вернёт Promise, который в итоге даст массив задач (если сервер ответил).

await приостанавливает выполнение текущей async-функции до завершения Promise. Он не блокирует весь браузер.

Урок 2 • Раздел 22

22. Обработка ошибок

Для сетевого и пользовательского кода ошибка — нормальный сценарий, а не исключение, которое можно игнорировать.

Контекст. В TaskFlow загрузка может упасть (сервер лёг, нет сети) — мы должны это перехватить и показать сообщение.

💡 Аналогия. try/catch — как попытка перейти улицу: «попробуй перейти (try), а если машина — отскочи (catch)». Программа не падает насовсем, а просто реагирует на проблему.
async function loadTasks() {
  try {
    const response = await fetch("http://localhost:3000/tasks");

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`); // сами бросаем ошибку
    }

    return await response.json();
  } catch (error) {
    console.error("Не удалось загрузить задачи", error);
    throw error; // пробрасываем дальше, чтобы вызвавший код тоже узнал
  }
}

Разбор по шагам:

  1. В try пытаемся загрузить данные.
  2. Если сервер ответил ошибкой — сами выбрасываем исключение через throw.
  3. Блок catch ловит любую ошибку и сообщает о ней.

Обратите внимание: fetch() не выбрасывает исключение только потому, что сервер вернул HTTP 404 или 500. Поэтому проверяем response.ok самостоятельно.

Обработка ошибок: try / catch / finally try const r = await fetch(url) const data = await r.json() render(data) // всё хорошо // код продолжается catch (error) ошибка в try? код ПЕРЕХОДИТ сюда console.error(error) showMessage(error) // НЕ прерываем программу // пользователь видит ошибку finally выполняется ВСЕГДА Паттерн для fetch try { const r = await fetch(url) if (!r.ok) throw new Error(r.status) return await r.json() } catch (e) { console.error("Ошибка:", e.message)
Рисунок. try/catch/finally: безопасный код при работе с сетью.
Урок 2 • Раздел 23

23. fetch и граница HTTP

fetch связывает JavaScript с HTTP. Frontend формирует запрос, получает Response и преобразует тело ответа, например, через response.json().

Контекст. В TaskFlow fetch — это «телефон» к серверу: мы звоним по адресу и просим список задач.

💡 Аналогия. fetch — как заказ в магазине: вы послали заявку, получили посылку (response) или отказ. Саму посылку (тело ответа) ещё нужно распаковать — методом .json().
// эти строки должны быть внутри async-функции
const response = await fetch("http://localhost:3000/tasks"); // звоним на сервер
const tasks = await response.json(); // распаковываем посылку в данные

console.log(tasks);

Что увидит пользователь: в консоли — массив задач, присланных сервером (или ошибка, если сервер не отвечает).

CORS. Если фронтенд запущен на порту 4200, а API на 3000, браузер заблокирует запрос из-за CORS (Cross-Origin Resource Sharing). Решение: настройте прокси или добавьте CORS-заголовки на сервере.
Важно. Адрес должен реально существовать. В нашем курсе mock API запускается локально на порту 3000. Поэтому в практических примерах мы используем абсолютный адрес, пока не настроим прокси.
Урок 2 • Раздел 24

24. Что JavaScript делает в браузере

Браузер предоставляет JavaScript API: DOM, events, fetch, timers, storage и другие возможности. Язык JavaScript сам по себе не определяет DOM. DOM и fetch предоставляет среда браузера.

Контекст. Понимание границы «что есть в языке, а что дал браузер» помогает не путаться, почему один и тот же код работает в браузере, но не в Node.js.

💡 Аналогия. JavaScript — как двигатель. А DOM, fetch, таймеры — как руль, колёса и фары, которые добавил «автозавод» (браузер). Двигатель сам по себе едет только на стенде.
ВозможностьОткуда берётсяПример
Array, Object, PromiseJavaScript language/runtimetasks.map(...)
documentBrowser APIdocument.querySelector
fetchWeb APIfetch(url)
setTimeoutWeb APIsetTimeout(fn, 1000)

Эта граница полезна, когда одна и та же программа запускается в браузере и в Node.js: набор доступных глобальных API отличается.

Урок 2 • Раздел 25

25. TypeScript: зачем нужен статический анализ

TypeScript проверяет JavaScript-программы до запуска и позволяет описать ожидаемые типы.

Официальный TypeScript Handbook определяет TypeScript как статический type checker для JavaScript-программ: он работает до выполнения и проверяет корректность типов.

Контекст. В TaskFlow TypeScript следит, чтобы мы случайно не передали в задачу число вместо текста.

💡 Аналогия. TypeScript — как внимательный корректор: он читает черновик до печати и подчёркивает ошибку красным. Сам он текст не печатает (это делает браузер), но бережёт от опечаток.
function greet(name: string) {
  return `Привет, ${name}`;
}

greet("Анна");
// greet(123); // ошибка типов: ожидается строка, а не число

Что увидит пользователь: рабочий вызов напечатает Привет, Анна; закомментированный вызов выдаст ошибку ещё до запуска в редакторе.

TypeScript не превращает JavaScript в «безопасный язык» и не проверяет реальный JSON с сервера автоматически. Он делает статические предположения, которые нужно поддерживать корректными.

TypeScript: от анализа к выполнению TypeScript .ts файлы типы, интерфейсы generics, union компилятор tsc ОШИБКИ ДО запуска нельзя присвоить строку числу JavaScript .js файлы динамические типы runtime проверки JSON с сервера ОШИБКИ ПРИ запуске undefined is not a function Что проверяет TypeScript? + правильность типов аргументов + существование свойств + совпадение интерфейсов + void / never возвраты - JSON с сервера - DOM API ответы - Пользовательский ввод
Рисунок. TypeScript проверяет исходный код. Runtime проверяет данные из внешнего мира.
Урок 2 • Раздел 26

26. Основные типы TypeScript

Для ежедневного frontend достаточно сначала уверенно знать string, number, boolean, массивы, объекты, union types и функции. Официальная документация TypeScript отдельно разбирает примитивы и функции как базовые конструкции.

Контекст. В TaskFlow: заголовок — string, количество — number, статус — boolean.

💡 Аналогия. Типы TypeScript — как бланк анкеты (внутренний контракт): в бланке написано, какие значения допустимы, и если вписать не то — корректор сразу заметит.
let title: string = "TaskFlow";    // только текст
let count: number = 10;            // только число
let completed: boolean = false;    // только да/нет

let tags: string[] = ["web", "frontend"]; // массив только из текста

В JavaScript число не разделяется на типы integer и float на уровне языка; в TypeScript соответствующий тип — number.

Урок 2 • Раздел 27

27. Type annotations и inference

Annotation — явное указание типа. Inference — способность TypeScript вывести тип самостоятельно.

Контекст. В TaskFlow часто пишут просто const count = 10 — и TypeScript сам понимает, что это number.

💡 Аналогия. Annotation — как подписать банку вручную («здесь соль»). Inference — как когда вы видите соль на вкус и не подписываете: TypeScript «попробовал» значение и сам понял тип.
const title: string = "Angular"; // annotation — подписали явно
const count = 10; // inference — TypeScript выводит number сам

const task = {
  id: 1,
  title: "React",
  completed: false
}; // TypeScript вывел форму объекта автоматически

Не нужно указывать тип там, где TypeScript очевидно его выводит. Ценность типов растёт на границах: функции, публичные API, модели данных и сложные структуры.

Урок 2 • Раздел 28

28. Arrays, objects и readonly

Типизация коллекций помогает определить, какие элементы разрешены.

Контекст. В TaskFlow поле id мы хотим защитить от случайного изменения — помечаем readonly.

💡 Аналогия. readonly — как пломба на счётчике: значение можно прочитать, но «вскрыть» и переписать через этот тип нельзя.
const titles: string[] = ["Angular", "React"]; // массив только строк

type Task = {
  readonly id: number; // id нельзя переназначить
  title: string;
  completed: boolean;
};

const task: Task = {
  id: 1,
  title: "TypeScript",
  completed: false
};

// task.id = 2; // ошибка: id защищён

readonly не делает объект глубоко неизменяемым, но защищает конкретное свойство от переназначения через данный тип.

Урок 2 • Раздел 29

29. Type aliases

Type alias даёт имя существующему описанию типа.

Контекст. В TaskFlow мы назовём «номер задачи» через TaskId, чтобы не писать number везде.

💡 Аналогия. Type alias — как прозвище: вместо длинного «человек ростом 180 см» вы говорите «Гигант». Смысл тот же, но короче и понятнее.
type TaskId = number; // прозвище для number

type Task = {
  id: TaskId;        // то же, что id: number
  title: string;
  completed: boolean;
};

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

Урок 2 • Раздел 30

30. Interfaces

Interface — способ описать форму объекта. В frontend он часто используется для моделей и входных параметров.

Контекст. В TaskFlow interface Task — это «чертёж» задачи, по которому проверяются все данные.

💡 Аналогия. Interface — как бланк анкеты: вверху написано, какие поля должны быть (имя, возраст). Заполнили не по бланку — корректор не пропустит.
interface Task {
  id: number;
  title: string;
  completed: boolean;
}

function renderTask(task: Task) {
  console.log(task.title); // знаем: у task точно есть title
}

TypeScript поддерживает и interfaces, и type aliases. Для большинства простых объектных моделей оба подхода подходят. В дальнейшем курс будет использовать оба, объясняя, почему выбран конкретный вариант.

Урок 2 • Раздел 31

31. Union types

Union type означает: значение может быть одним из нескольких допустимых типов.

Контекст. В TaskFlow статус задачи — только один из трёх: "todo" | "doing" | "done".

💡 Аналогия. Union type — как переключатель света с тремя фиксированными положениями. Поставить его в четвёртое, несуществующее положение, нельзя.
type Status = "todo" | "doing" | "done"; // только эти три значения

let status: Status = "todo";

status = "done";
// status = "cancelled"; // ошибка: такого варианта нет в union

Union types особенно полезны для UI-статусов. Вместо произвольной строки мы ограничиваем возможные состояния. Здесь мы используем строки как типы — это называется literal types и подробно разбирается в следующем разделе.

Урок 2 • Раздел 32

32. Literal types и discriminated unions

Literal type ограничивает значение конкретным литералом. Несколько литералов можно объединить в union.

Контекст. В TaskFlow состояние загрузки удобно описать через kind: «loading / success / error».

💡 Аналогия. Discriminated union — как конверт с пометкой: на нём написано «loading», «success» или «error», и по этой пометке вы сразу знаете, что внутри лежит.
// предполагается, что тип Task определён в другом файле
type Priority = "low" | "medium" | "high";

type TaskState =
  | { kind: "loading" }                          // грузим
  | { kind: "success"; tasks: Task[] }           // успех: есть задачи
  | { kind: "error"; message: string };          // ошибка: есть текст

Поле kind называется discriminant. По нему TypeScript может сузить тип внутри условия. Такой подход особенно полезен для состояния загрузки, ошибки и успеха.

Урок 2 • Раздел 33

33. Optional properties и null

Знак ? означает, что свойство может отсутствовать.

Контекст. В TaskFlow фильтр может не иметь поискового слова — тогда поле search просто не указано.

💡 Аналогия. ? — как необязательная графа в анкете: её можно оставить пустой, и бланк всё равно примут.
type Priority = "low" | "medium" | "high";

interface TaskFilter {
  search?: string;   // может быть, а может не быть
  priority?: Priority;
}

Null и undefined — разные состояния. Например, undefined может означать отсутствие свойства или отсутствие присвоенного значения, а null — явно заданное отсутствие значения.

💡 Аналогия. undefined — вы ещё не заполнили графу. null — вы специально написали «здесь пусто».
function printOwner(owner: string | null) {
  if (owner === null) {
    console.log("Без владельца");
    return; // выходим, если владельца нет
  }

  console.log(owner); // здесь owner точно строка
}
Урок 2 • Раздел 34

34. Function types

В TypeScript можно описывать не только данные, но и форму функции.

Контекст. В TaskFlow мы опишем «предикат задачи» — функцию, которая говорит, подходит ли задача.

💡 Аналогия. Function type — как объявление вакансии: «нужен сотрудник, который берёт задачу и возвращает да/нет». Кто именно придёт — неважно, важна форма.
// предполагается, что тип Task определён в другом файле
type TaskPredicate = (task: Task) => boolean; // берёт Task, возвращает true/false

const isCompleted: TaskPredicate =
  task => task.completed; // подходит: берёт задачу, возвращает boolean

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

Официальная документация TypeScript описывает сигнатуры функций как отдельный типовой механизм.

Урок 2 • Раздел 35

35. Generics

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

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

Контекст. В TaskFlow first<Task> вернёт именно задачу, а не «что-то непонятное».

💡 Аналогия. Generic — как универсальная коробка: вы указываете, какой тип положить внутрь (например, Task), и на выходе получаете ровно этот тип. Тип «протягивается» сквозь функцию.
function first<T>(items: T[]): T | undefined {
  return items[0]; // возвращаем первый элемент (или undefined)
}

const firstTask = first(tasks);       // TypeScript знает: Task | undefined
const firstTitle = first(["Angular", "React"]); // знает: string | undefined

В первом случае TypeScript выводит Task | undefined, во втором — string | undefined. Официальный Handbook показывает generics как способ сохранять связь между типами входных и выходных данных.

Урок 2 • Раздел 36

36. Generic Task API

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

Контекст. В TaskFlow мы сделаем универсальную getJson<T>, которая загружает данные любого типа.

💡 Аналогия. getJson<T> — как универсальная коробка: вы говорите «привезите мне именно Task[]», и на выходе получаете ровно этот тип, а не «коробку непонятно чего».
// import type { Task } from "./task";
async function getJson<T>(url: string): Promise<T> {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`); // ошибка сети/сервера
  }

  return await response.json() as T; // говорим TypeScript: результат типа T
}

const tasks = await getJson<Task[]>(
  "http://localhost:3000/tasks"
);

Здесь generic говорит вызывающему коду, какой тип результата он ожидает. Но важно: type assertion не проверяет фактический JSON. Это только утверждение для type checker. Поэтому в production при недоверенных данных могут использоваться runtime-схемы или валидаторы.

💡 Зачем generics связывают вход и выход. Без <T> вы бы написали getJson(url): any и потеряли тип результата. Generic делает ровно обратное: тип, который вы указываете при вызове (getJson<Task[]>), «протягивается» на возвращаемое значение. Входной параметр и выход оказываются соединены одной переменной типа — отсюда и польза автодополнения.

Песочница: generics на практике

▶️ Попробуйте сами: функция first без generic теряет тип элемента. В TypeScript запись first<Task>(tasks) вернула бы именно Task, а не any.

Песочница: нажми «Запустить» — и увидишь, как функция first() возвращает первый элемент массива, и почему без generic TypeScript теряет тип. Меняй код и запускай снова.
Песочница: generics (first<T>)
Урок 2 • Раздел 37

37. any, unknown и почему они разные

any практически отключает типовую проверку для значения. unknown наоборот говорит: «мы пока не знаем тип, поэтому сначала проверь». Для внешних данных unknown безопаснее как стартовая точка.

Контекст. В TaskFlow данные с сервера сначала неизвестны — берём unknown, а не any.

💡 Аналогия. any — как «всё равно что внутри», вы открыли дверь и ничего не проверяете. unknown — как «сначала спросим, что внутри, прежде чем трогать».
let unsafe: any = "hello";
unsafe.notExisting.method(); // TypeScript не защищает — может упасть при запуске

let external: unknown = "hello";

// external.toUpperCase(); // ошибка: тип неизвестен

if (typeof external === "string") {
  external.toUpperCase(); // только после проверки — можно
}

Что увидит пользователь: со строкой после проверки toUpperCase() вернёт HELLO; а any пропустит даже заведомо сломанный вызов.

Правило курса. Не используйте any как способ убрать ошибку. Сначала выясните, какой тип действительно нужен.
Урок 2 • Раздел 38

38. Type assertions и осторожность

Type assertion сообщает TypeScript, что вы как разработчик считаете значение конкретного типа.

Контекст. В TaskFlow иногда мы «знаем лучше компилятора», что значение — строка, и пишем as string.

💡 Аналогия. Type assertion — как наклейка «соль» на банку: вы сами решили, что внутри соль. Реально там может быть сахар — наклейка ничего не проверяет.
const value: unknown = "Angular";

const text = value as string; // говорим TypeScript: доверяй, это string

Assertion не изменяет runtime-значение и не проверяет его. Поэтому это инструмент доверия, а не валидации.

Опасность. Если вам постоянно приходится писать as SomeType, скорее всего, ваша модель данных определена неверно. Type assertion не устраняет проблему — он её скрывает.
Урок 2 • Раздел 39

39. TypeScript и JSON из API

JSON приходит во время выполнения. TypeScript-код не может «превратить» неправильный JSON в правильный только потому, что вы написали интерфейс.

Контекст. В TaskFlow сервер может прислать «кривой» ответ — TypeScript этого до запуска не узнает.

💡 Аналогия. Интерфейс — как ожидаемый чертёж. Но если фабрика прислала деталь не по чертежу, сам чертёж её не исправит. Нужна отдельная проверка (runtime validation).
interface Task {
  id: number;
  title: string;
  completed: boolean;
}

const raw: unknown = await response.json(); // что там — не знаем до проверки

Следующий уровень зрелости frontend — runtime validation: проверить, что raw действительно имеет ожидаемую форму. Сегодня нам важно запомнить границу: TypeScript контролирует исходный код, а серверные данные нужно рассматривать как внешние.

Пример. Чтобы безопасно использовать raw, проверяем его форму: if (typeof raw === "object" && raw !== null && "title" in raw) { ... }. Позже в курсе познакомимся с библиотеками вроде Zod, которые упрощают runtime-валидацию.
Урок 2 • Раздел 40

40. Архитектура модели Task

Прежде чем писать Angular и React-компоненты, определим единый тип Task. Тогда две реализации приложения будут работать с одной логической моделью.

Контекст. Один общий «чертёж» задачи используется и в Angular, и в React — это связующая нить курса.

💡 Аналогия. Модель Task — как общий ГОСТ детали: и на заводе А (Angular), и на заводе Б (React) деталь делают по одному стандарту, поэтому они взаимозаменяемы.
export type Priority = "low" | "medium" | "high";

export interface Task {
  id: number;
  title: string;
  description?: string;
  completed: boolean;
  priority: Priority;
  createdAt: string;
}
Слои TaskFlow: от API до экрана API JSON от сервера fetch() → Response response.json() внешние данные мы не контролируем Model interface Task type Priority контракт данных общий для Angular и React State tasks: Task[] loading: boolean error: string | null временное состояние меняется при действиях UI TaskList TaskItem TaskForm отображает state отправляет события
Рисунок 2. Одна модель данных проходит через несколько слоёв frontend.

Такой контракт станет общей нитью всего курса.

Урок 2 • Раздел 41

41. Практика: JavaScript-версия TaskFlow

Сначала создадим маленькую версию логики без Angular и React. Цель — закрепить язык до перехода к framework.

▶️ Ниже — песочница, где можно запустить весь пример. А пока разберём код по шагам.

Шаг 1. Создайте файл tasks.js

Контекст. Массив задач здесь играет роль «базы данных» в памяти браузера.

💡 Аналогия. Этот массив — как список дел на листочке: мы можем зачёркивать, добавлять и фильтровать записи.
// Массив задач — «база данных» в памяти для примера
const tasks = [
  { id: 1, title: "Изучить JS", completed: false },   // completed: false — задача ещё активна
  { id: 2, title: "Изучить TS", completed: true },     // completed: true — задача выполнена
  { id: 3, title: "Создать Angular", completed: false }
];

// items — произвольный массив задач; фильтруем только невыполненные
function getActiveTasks(items) {
  // filter оставляет элементы, для которых предикат вернул true
  return items.filter(task => !task.completed);
}

// Выводим в консоль результат фильтрации
console.log(getActiveTasks(tasks));

Что увидит пользователь: в консоли останутся только невыполненные задачи (id 1 и 3).

Шаг 2. Добавьте функцию toggleTask

💡 Аналогия. toggleTask — как переключатель света: нажал — загорелось, нажал ещё раз — погасло. При этом старый список не переписываем, а делаем новый (map + spread).
// items — исходный массив, id — какую задачу переключить
function toggleTask(items, id) {
  // map создаёт НОВЫЙ массив, старый не меняем
  return items.map(task =>
    // если id совпал — инвертируем completed через spread-копию
    task.id === id
      ? { ...task, completed: !task.completed }
      : task // иначе возвращаем задачу без изменений
  );
}
Песочница: нажми «Запустить» — и увидишь список всех задач, только активные и результат переключения статуса через toggleTask. Меняй код и запускай снова.
Песочница: JavaScript-версия TaskFlow
Урок 2 • Раздел 42

42. Практика: TypeScript-модель Task

Теперь перенесём модель в TypeScript.

Контекст. Опишем «чертёж» задачи в отдельном файле, чтобы потом переиспользовать в Angular и React.

✍️ Действие: создайте файл src/app/models/task.ts (или src/types/task.ts в React)

💡 Аналогия. Файл модели — как паспорт задачи: в нём прописано, какие поля обязательны, а какие нет.
// Union type: приоритет — только одно из трёх допустимых значений
export type Priority = "low" | "medium" | "high";

// Общий контракт задачи для всего курса
export interface Task {
  id: number;          // уникальный числовой идентификатор
  title: string;       // заголовок задачи
  completed: boolean;  // выполнена ли задача (НЕ путать с TaskFilter "done")
  priority: Priority;  // приоритет из union type выше
}

Шаг 2. Проверьте ошибку типов

Контекст. Специально напишем «неправильный» приоритет, чтобы увидеть, как TypeScript нас остановит.

💡 Аналогия. Это как попытка вписать в графу «приоритет» слово «срочно», хотя в бланке разрешены только «low / medium / high». Корректор сразу подчеркнёт.
// Аннотация типа: ожидаем объект, соответствующий Task
const task: Task = {
  id: 1,
  title: "TypeScript",
  completed: false,
  priority: "urgent" // ошибка: "urgent" нет в union Priority
};

Редактор должен сообщить, что urgent не входит в union Priority.

Именно здесь ощущается ценность TypeScript: допустимые значения становятся частью контракта.
Урок 2 • Раздел 43

43. Практика: фильтрация и преобразование задач

Используем типизированные функции.

Контекст. Напишем три функции для TaskFlow: «только выполненные», «только заголовки», «найти по id» — и укажем их типы.

💡 Аналогия. Каждая функция — как сотрудник с должностной инструкцией: написано, что на входе, что на выходе. TypeScript проверяет, чтобы инструкция соблюдалась.
// import type { Task } from "./task";
// Вход: массив Task. Выход: тоже Task[] (только выполненные)
function getCompleted(tasks: Task[]): Task[] {
  return tasks.filter(task => task.completed); // оставляем completed === true
}

// Вход: Task[]. Выход: string[] — массив заголовков
function getTitles(tasks: Task[]): string[] {
  return tasks.map(task => task.title); // преобразуем каждую задачу в строку
}

// Вход: массив и id. Выход: Task | undefined — задача может не найтись
function findTask(
  tasks: Task[],
  id: number
): Task | undefined {
  return tasks.find(task => task.id === id); // первый совпавший или undefined
}

Задача студента — объяснить, почему return type указан именно так.

Песочница: нажми «Запустить» — и увидишь, что выдают функции getCompleted, getTitles и findTask на примере задач. Меняй код и запускай снова.
Песочница: TypeScript-функции
Урок 2 • Раздел 44

44. Практика: mock API и async/await

Используйте mock API из первого занятия. Если он ещё не запущен:

cd taskflow-api
npm install
npm run api

Контекст. Теперь свяжем нашу модель с реальным сервером: загрузим задачи через fetch.

💡 Аналогия. Mock API — как тренажёр самолёта: всё как в бою, но рисков нет. Мы тренируемся грузить данные, пока нет настоящего бэкенда.
// import type { Task } from "./task";
// async → функция возвращает Promise; тип результата Promise<Task[]>
async function loadTasks(): Promise<Task[]> {
  // await приостанавливает функцию до получения Response
  const response = await fetch(
    "http://localhost:3000/tasks"
  );

  // fetch НЕ падает при 404/500, поэтому проверяем сами
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`); // явная ошибка → попадёт в catch
  }

  // await + .json() парсит тело ответа в массив задач
  return await response.json();
}

Разбор по шагам:

  1. Ждём ответа сервера (await fetch).
  2. Если статус не OK — бросаем ошибку.
  3. Читаем тело как JSON и возвращаем массив задач.

Здесь соединяются сразу несколько тем: function, Promise, generic-like return annotation, fetch, JSON и Task.

Урок 2 • Раздел 45

45. Практика: обработка ошибок

Добавим отдельную функцию верхнего уровня.

Контекст. «Обертка» run() ловит любые ошибки загрузки и показывает понятное сообщение.

💡 Аналогия. run() — как дежурный у входа: если курьер не пришёл, дежурный сообщает посетителям, что случилось, вместо того чтобы молча рухнуть.
// Верхнеуровневая обёртка: сама ничего не знает про сеть
async function run() {
  try {
    // здесь выполняется «счастливый» сценарий
    const tasks = await loadTasks(); // если loadTasks бросит ошибку — уйдём в catch

    console.log("Загружено:", tasks.length);
  } catch (error) {
    // сюда попадаем при любой ошибке выше (сеть, !ok, JSON)
    console.error("Ошибка загрузки", error);
  }
}

run(); // запускаем верхнеуровневую функцию

Попробуйте временно изменить порт API с 3000 на 3001. Посмотрите, что произойдёт. Затем верните правильный порт.

Песочница: нажми «Запустить» — и увидишь загрузку данных по сети через fetch и async/await (или сообщение об ошибке). Меняй код и запускай снова.
Песочница: fetch + async/await
Порядок ошибок. Если сервер не запущен — ошибка в catch. Если сервер вернул 404/500 — ошибка в if (!response.ok). Если JSON битый — ошибка в response.json(). Каждый уровень нужно обрабатывать отдельно.

Проверка

Вы должны уметь ответить: ошибка возникла в TypeScript-коде, в браузере, на HTTP-уровне или потому, что сервер не запущен?

Урок 2 • Раздел 46

46. Практика: подготовка к Angular

Контекст. В следующих уроках тот же Task будет использовать Angular. Уже сейчас подготовим файл модели, чтобы не переделывать потом.

✍️ Действие: создайте файл src/app/models/task.ts и перенесите в него интерфейс Task из раздела 42.
💡 Аналогия. Это как заранее заготовить выкройку: когда дойдём до пошива (Angular-компонента), останется только приложить ткань.
src/app/models/task.ts

В Angular файл можно импортировать через обычный ES module import:

import type { Task } from "./models/task";

Это важный момент: язык и типовая модель не являются «собственностью React» или «собственностью Angular». Они общие.

Урок 2 • Раздел 47

47. Практика: подготовка к React

Контекст. В React тип модели тоже хранится отдельно; компонент получает задачу через props.

✍️ Действие: создайте файл src/types/task.ts с тем же интерфейсом Task.
💡 Аналогия. Props — как передача записки компоненту: «вот тебе задача, покажи её на экране». Компонент сам по себе задачу не придумывает.
src/types/task.ts
import type { Task } from "./types/task";

Компонент получает Task через props:

function TaskItem({ task }: { task: Task }) {
  return (
    <article>
      <h2>{task.title}</h2>
    </article>
  );
}

Что увидит пользователь: на экране появится заголовок задачи внутри карточки article с подзаголовком h2.

Именно поэтому TypeScript удобно изучать до framework: один и тот же язык работает в обеих экосистемах.

💡 Связь с Уроками 3–4. В Уроке 3 (Angular) этот же Task станет моделью компонента и основой для *ngFor/сигналов, а в Уроке 4 (React) — пропсами TaskItem и элементом useState<Task[]>. Поэтому правильно описанный интерфейс сейчас сэкономит часы позже.
Урок 2 • Раздел 48

48. Самостоятельная работа

Контекст. Закрепите темы урока: типы, приоритеты, обновление задач, generics и загрузку с API. Ниже — задания и песочница, где можно потренироваться.

💡 Аналогия. Домашняя работа — как тренировка после теории: мышцы (понимание кода) растут, только когда вы напишете его сами.

Задание 1. Тип Task

Добавьте поле assignee, которое может быть строкой или null.

Задание 2. Приоритет

Создайте функцию getHighPriorityTasks.

Задание 3. Обновление

Создайте функцию renameTask, которая возвращает новую задачу и не изменяет исходную.

Задание 4. Generic

Напишите first<T>, возвращающую первый элемент массива или undefined.

Задание 5. API

Загрузите Task[] из mock API и выведите количество задач с priority === "high".

Задание 6. Ошибка

Сделайте так, чтобы функция загрузки выбрасывала ошибку при статусе 404/500.

Песочница: нажми «Запустить» — и увидишь решения заданий самостоятельной работы (assignee, приоритет, renameTask, first). Меняй код и запускай снова.
Песочница: попробуйте задания
Урок 2 • Раздел 49

49. Подсказки к самостоятельной работе

Контекст. Если застряли — сверьтесь с образцами. Главное: типы указывают, что ожидается на входе и выходе.

Задание 1.
interface Task {
  id: number;
  title: string;
  completed: boolean;
  priority: Priority;
  assignee: string | null; // добавленное поле
}
Задание 2.
function getHighPriorityTasks(tasks: Task[]) {
  return tasks.filter(task => task.priority === "high");
}
Задание 3.
function renameTask(task: Task, title: string): Task {
  return { ...task, title };
}
Задание 4.
function first<T>(items: T[]): T | undefined {
  return items[0];
}
Задание 5.
const tasks = await loadTasks();
const high = tasks.filter(task => task.priority === "high");
console.log(high.length);
Задание 6.
if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

response.ok равно false для всех статусов вне диапазона 200-299, включая 404 и 500.

Совет: сначала сравните идею с вашим решением, а уже потом смотрите код целиком.

Урок 2 • Раздел 50

50. Контрольные вопросы

  1. Почему const не означает неизменяемый объект?
  2. Что такое область видимости?
  3. Чем map отличается от filter?
  4. Что такое callback?
  5. Зачем нужны ES modules?
  6. Что возвращает async-функция?
  7. Что такое Promise?
  8. Почему нужно проверять response.ok?
  9. Что проверяет TypeScript до запуска?
  10. Что такое type inference?
  11. Чем interface отличается от обычного объекта runtime?
  12. Что такое union type?
  13. Что делает readonly?
  14. Для чего нужны generics?
  15. Почему unknown безопаснее any?
  16. Может ли TypeScript проверить, что JSON с API реально соответствует интерфейсу?
  17. Что такое type assertion?
  18. Почему spread полезен при обновлении state?
  19. Как функция может вернуть новый объект, не изменяя исходный?
  20. Какая связь между TypeScript и Angular/React?
Урок 2 • Раздел 51

51. Мини-словарь

Краткий справочник терминов урока. Если забыли аналогию — вернитесь к нужному разделу выше.

ТерминПростое объяснение
VariableИмя, связанное со значением (как ячейка в шкафчике).
ScopeОбласть, в которой имя доступно (как комната).
PrimitiveБазовый тип значения JavaScript (string, number, boolean…).
CallbackФункция, переданная другой функции (как номерок в пиццерии).
ClosureФункция, сохраняющая доступ к внешним переменным (блокнот в кармане).
ModuleФайл с import/export (коробка с инструментами).
PromiseОбъект, представляющий будущий результат async-операции (обещание + ожидание посылки).
async/awaitСинтаксис для работы с Promise (обещание + ожидание посылки).
Type annotationЯвное указание типа (подписали банку).
InferenceАвтоматический вывод типа (поняли на вкус).
InterfaceОписание формы объекта в TypeScript (бланк анкеты).
Type aliasИмя для типа (прозвище).
UnionТип, допускающий несколько вариантов (переключатель).
Literal typeТип, допускающий конкретное значение.
GenericПараметризованный тип, сохраняющий связь между типами (универсальная коробка).
anyТип, резко ослабляющий типовую проверку (не проверяем).
unknownНеизвестный тип, который нужно проверить перед использованием.
AssertionУказание TypeScript, какой тип вы предполагаете (наклейка).
RuntimeМомент фактического выполнения программы.
Урок 2 • Раздел 52

52. Чек-лист урока

Если вы можете выполнить все шесть самостоятельных заданий без подсказок, база для следующего занятия сформирована.
Урок 2 • Раздел 53

53. Источники и материалы для продолжения

Материал основан на официальной документации и общих механизмах JavaScript/TypeScript.

ИсточникЧто читать
TypeScript HandbookОсновы языка и типовая система
TypeScript Everyday TypesПримитивы, массивы, функции, object types
TypeScript GenericsОбобщённые функции и типы
MDN PromisesPromise, chaining, async/await, ошибки
MDN JavaScript GuideОсновы JavaScript и модули

Актуальность веб-ссылок и терминологии сверена с официальными материалами на август 2026 года. TypeScript Handbook обновлялся в июле 2026 года. MDN описывает Promise и async/await как фундамент современной асинхронной модели JavaScript.

Куда дальше: закрепи JS/TS в практике к уроку 2, а дальше — данные и события в Уроке 4.

📖 Глоссарий

ТерминЖизненная аналогияКоротко
let / constЯчейка в шкафчике с табличкой-именемИмя для значения. const нельзя переназначить, let — можно.
МассивПолка с коробкамиУпорядоченная коллекция значений (наш список задач).
filterСитечко для чаяПропускает только подходящие элементы, остальное отбрасывает.
mapКонвейер (ксерокс)Каждый элемент проходит операцию и выходит преобразованным.
reduceНакопительный итогЗначения по очереди складываются в один общий результат.
findПоиск по номеруНаходит один элемент по условию (например, по id).
Типы (TypeScript)Бланк анкетыОписывают, какие значения допустимы, и подсвечивают ошибки до запуска.
ИнтерфейсБланк анкеты (чертёж)Форма объекта: какие поля обязательны, а какие нет.
Promise / async-awaitОбещание + ожидание посылкиРезультат придёт позже — либо успех, либо ошибка.
GenericsУниверсальная коробкаПараметр типа «протягивается» сквозь функцию (например, first<Task>).

🧠 Самопроверка

1. let означает, что значение переменной вообще нельзя изменить. Верно или неверно?

2. map оставляет только подходящие элементы, а filter преобразует каждый элемент. Верно или неверно?

3. Promise — это обещание получить результат позже, как посылку. Верно или неверно?

Прогресс курса