Практическая работа к уроку 2 (занятие 2)

JavaScript и TypeScript на практике

Превратите теорию урока 2 в рабочий код: массивы задач, типы, интерфейсы, async/await и обработка ошибок.

💡 Этот файл — тренировка. Мы строим маленький «TaskFlow»: список задач, как в телефоне. Каждый кусок кода разбираем по шагам, как на занятии у доски.

Что вы узнаете

Как читать этот урок. У каждого блока кода сначала — бытовая аналогия (как это работает в жизни), потом пошаговый разбор, потом что вы увидите. Так новичку проще «зацепиться» за смысл.

Задание 1. Список задач и массивы

Дан массив задач. Выполните четыре операции и выведите результаты.

Аналогия: массив — это полка с коробками. Представьте стеллаж, где каждая коробка — отдельная задача. У коробки есть номер (поле id), надпись (поле title), пометка «сделано/не сделано» (поле completed) и важность (поле priority). Методы вроде filter и map — это действия с полкой: отобрать, переписать, найти, посчитать.

Контекст. В TaskFlow весь список задач хранится именно как массив объектов. Здесь мы учимся брать этот массив и получать из него нужную информацию без ручного перебора.

const tasks = [
  { id: 1, title: "Изучить JS", completed: false, priority: "high" },
  { id: 2, title: "Изучить TS", completed: true,  priority: "low" },
  { id: 3, title: "Создать Angular", completed: false, priority: "medium" }
];

// 1. Активные задачи (completed === false)
//    filter — как ситечко для чая: пропускает только подходящие
const active = tasks.filter(t => !t.completed);

// 2. Только заголовки
//    map — как конвейер: берёт каждую задачу и возвращает только её название
const titles = tasks.map(t => t.title);

// 3. Найти задачу с id === 2
//    find — как поиск коробки по номеру на полке
const found = tasks.find(t => t.id === 2);

// 4. Сколько выполнено
//    reduce — как накопительный итог: идём по всем и суммируем результат
const doneCount = tasks.reduce(
  (count, t) => count + (t.completed ? 1 : 0), 0
);

console.log({ active, titles, found, doneCount });
Аналогии для каждой операции:
  • filter — ситечко для чая: проливает только те задачи, где completed равно false.
  • map — конвейер/ксерокс: на входе задача, на выходе только её название.
  • find — поиск по номеру: берёт первую коробку с нужным id и останавливается.
  • reduce — накопительный итог: бежит по списку и складывает единицы там, где задача выполнена.

Пошаговый разбор:

  1. Создаём массив tasks из трёх объектов-задач.
  2. filter проходит по каждой задаче и оставляет только те, где !t.completed (не выполнено). Останутся задачи 1 и 3.
  3. map берёт каждую задачу и возвращает лишь t.title — получаем массив строк.
  4. find возвращает первую задачу с id === 2 (это «Изучить TS»).
  5. reduce начинает с 0 и прибавляет 1 за каждую выполненную задачу. Выполнена только задача 2 — итог 1.

Что увидит студент в консоли:

{
  active:   [ {id:1,...}, {id:3,...} ],  // две невыполненные
  titles:   ["Изучить JS", "Изучить TS", "Создать Angular"],
  found:    { id: 2, title: "Изучить TS", completed: true, priority: "low" },
  doneCount: 1
}
Проверка. active.length должен быть 2, doneCount — 1, а found.title — «Изучить TS».

Задание 2. Типы и интерфейсы

Опишите модель Task и функцию, которая возвращает только активные задачи. Укажите типы явно.

Аналогия: interface — это бланк анкеты. Представьте, что перед выдачей коробки на полку вы заполняете бланк: в графе «id» — только число, в «title» — только текст, в «completed» — галочка (да/нет), а в «priority» — один из трёх вариантов. Если кто-то запишет в «id» слово «привет» — анкета не примется. Так же TypeScript проверяет данные до запуска.

Контекст. В большом приложении (Angular/React) у задачи должна быть чёткая форма. Интерфейс Task — это «контракт»: какие поля обязательны и какого они типа. Функция getActive берёт массив таких задач и отдаёт только невыполненные.

// Бланк анкеты задачи: какие поля бывают и какого они типа
interface Task {
  id: number;                       // номер — только число
  title: string;                    // название — только текст
  completed: boolean;               // сделано или нет — true/false
  priority: "low" | "medium" | "high"; // важность — один из трёх вариантов
}

// Функция получает массив Task и возвращает тоже массив Task
function getActive(tasks: Task[]): Task[] {
  return tasks.filter(t => !t.completed); // оставляем невыполненные
}

// Пример вызова
const result = getActive([
  { id: 1, title: "A", completed: false, priority: "high" },
  { id: 2, title: "B", completed: true,  priority: "low" }
]);

Пошаговый разбор:

  1. Объявляем интерфейс Task — описание «бланка».
  2. Пишем функцию getActive, помечая вход (tasks: Task[]) и выход (: Task[]) типами.
  3. Внутри используем уже знакомый filter, чтобы отсеять выполненные.
  4. Вызываем функцию с двумя задачами — в result попадёт только задача с id: 1.

Что увидит студент: переменная result содержит один объект { id: 1, title: "A", completed: false, priority: "high" }. А если написать priority: "urgent" — TypeScript подчеркнёт ошибку ещё до запуска.

Почему это важно. В Angular и React тип Task станет контрактом между сервисом, компонентом и шаблоном. Ошибку в имени поля (например, titel вместо title) TypeScript покажет до запуска — и вы исправите её за секунду, а не ловили бы в рабочем приложении.

Задание 3. Generics

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

Аналогия: generics — это универсальная коробка. Представьте коробку, в которую можно положить что угодно — яблоко, книгу, шарик, — но важно, чтобы внутри лежало одно и то же. Буква T (от слова Type) — это «пока не знаю что, но один вид». Когда вы кладёте строки — коробка становится «коробкой строк», кладёте задачи — «коробкой задач». Функция first просто достаёт первый предмет из любой такой коробки.

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

// T — «любой тип», который подставится при вызове
function first<T>(items: T[]): T | undefined {
  return items[0]; // вернёт первый элемент или undefined, если пусто
}

const title = first(["Изучить JS", "Изучить TS"]); // string | undefined
const task  = first([{ id: 1, title: "A", completed: false, priority: "high" }]);

Пошаговый разбор:

  1. Объявляем first<T>: на вход массив элементов типа T, на выход — один такой элемент или undefined.
  2. Вызываем с массивом строк — TypeScript понимает, что T это string, значит title имеет тип string | undefined.
  3. Вызываем с массивом объектов — теперь T это наш объект задачи, и task имеет соответствующий тип.

Что увидит студент: в title попадёт "Изучить JS", а в task — объект { id: 1, title: "A", completed: false, priority: "high" }. Редактор подскажет поля объекта, потому что тип известен.

Вопрос. Чему равен тип task? (Подсказка: это объект с полями id, title, completed, priority.)

Задание 4. async/await и ошибки

Сделайте запрос к mock-серверу и корректно обработайте HTTP-ошибку и сетевой сбой.

Аналогия: Promise / async-await — это обещание + ожидание посылки. Представьте, что вы заказали товар в интернет-магазине: продавец обещает прислать посылку, а await — вы стоите у двери и ждёте курьера, не делая ничего другого в этой строчке кода. Когда посылка пришла — вы её распаковываете (response.json()). А try/catch — это страховка: если магазин закрылся или посылка потерялась, вы не падаете в обморок, а спокойно записываете «не получилось» и продолжаете жить (возвращаете пустой список).

Контекст. В реальном TaskFlow задачи обычно приходят с сервера, а не лежат в коде. Здесь мы учимся запрашивать их и быть готовыми к двум бедам: сервер ответил «ошибка» (404/500) и сервер вообще не отвечает (нет сети).

// async — функция умеет «ждать»; Promise — это обещание прислать результат
async function loadTasks(): Promise<unknown> {
  try {
    // ждём ответа от сервера, как посылку от курьера
    const response = await fetch("http://localhost:3000/tasks");

    // сервер может ответить "не найдено" (404) или "ошибка" (500)
    // fetch сам НЕ бросает ошибку на такие ответы — проверяем вручную
    if (!response.ok) {
      throw new Error("HTTP " + response.status);
    }

    // распаковываем посылку: переводим JSON в данные
    return await response.json();
  } catch (error) {
    // Сюда попадём и при 404/500, и при выключенном сервере
    console.error("Не удалось загрузить:", error);
    return []; // безопасный итог: пустой список, приложение не падает
  }
}

Пошаговый разбор:

  1. Помечаем функцию async, чтобы внутри работал await.
  2. В блоке try делаем fetch и ждём ответа через await.
  3. Проверяем response.ok: если статус неуспешный — сами бросаем ошибку через throw.
  4. Если всё хорошо — возвращаем распакованные данные (response.json()).
  5. Если где-то случилась беда — управление переходит в catch, мы логируем ошибку и возвращаем пустой массив, чтобы код дальше работал.

Что увидит студент: при работающем сервере — загруженные задачи; при выключенном сервере — в консоли сообщение «Не удалось загрузить: …» и пустой список вместо аварийного завершения.

Важно. Без if (!response.ok) fetch НЕ бросит ошибку при статусе 404/500 — код посчитает их успехом и попытается распаковать «мусор» как JSON.

Интерактивная песочница

Отредактируйте код и нажмите Запустить (или Ctrl+Enter):

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

Контекст. Здесь живой TaskFlow на чистом JavaScript. Слева — код, справа — что он рисует на странице. Функции getActiveTasks и toggleTask повторяют то, что мы разбирали выше, но прямо в браузере.

Песочница: нажми «Запустить» — и увидишь живой список задач TaskFlow: сколько всего, какие активные и как меняется задача после toggleTask. Меняй код и запускай снова.
Песочница: JavaScript-версия TaskFlow
▶️ Попробуйте сами. Поменяйте в массиве completed у задачи 1 на true и запустите — галочка в списке изменится. Или вызовите toggleTask(tasks, 2) и посмотрите, как меняется вторая задача.

Проверьте себя

Вопросы.
  1. Чем отличаются map и filter? (Подсказка: map преобразует каждый элемент и сохраняет их количество, а filter только отбирает — и может стать меньше.)
  2. Почему find может вернуть undefined и как это учесть? (Подсказка: если задачи с таким id нет, результат будет пустым — проверяйте перед использованием.)
  3. Почему fetch не бросает ошибку при 404? (Подсказка: для браузера ответ «страница не найдена» — это тоже успешный ответ, поэтому статус проверяют вручную через response.ok.)

Практика к уроку 2. Далее: Практика к уроку 3 →

📖 Глоссарий

ТерминЖизненная аналогияКоротко
массивполка с коробкамиупорядоченный набор задач, к каждой можно обратиться по номеру
filterситечко для чаяпропускает только подходящие элементы, количество уменьшается
mapконвейер / ксероксберёт каждый элемент и возвращает преобразованный, число не меняется
findпоиск по номерунаходит первую подходящую коробку и останавливается (может вернуть undefined)
reduceнакопительный итогбежит по всем и складывает результат в одно значение
типы / interfaceбланк анкетыконтракт: какие поля обязательны и какого они типа, ошибку видно до запуска
Promise / async-awaitобещание + ожидание посылкиждём результат от сервера; try/catch — страховка при сбое

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

1. Метод filter сохраняет то же количество элементов, что и в исходном массиве. Верно или неверно?

2. Если вызвать tasks.find(t => t.id === 999), а задачи с таким номером нет, метод вернёт undefined. Верно или неверно?

3. В TypeScript интерфейс Task помогает поймать опечатку в имени поля (например, titel вместо title) уже после запуска приложения. Верно или неверно?

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