Перейти к основному содержимому

Асинхронное программирование в TypeScript

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

Этот урок идёт от базового async/await к более продвинутым приёмам.

Примечание: Урок предполагает, что вы уже знакомы с основами промисов (Promise) и асинхронного программирования в JavaScript. Если эти идеи для вас новы, сначала изучите асинхронность в JavaScript.


Промисы в TypeScript

TypeScript делает промисы типобезопасными с помощью дженериков.

Promise<T> — это операция, которая завершится позже значением типа T либо завершится неудачей с причиной типа any.

Ключевые моменты:

  • Promise<T> — обобщённый тип, где T — тип разрешённого значения.
  • Promise<void> — для промисов, которые не возвращают значение.
  • Promise<never> — для промисов, которые никогда не разрешаются (встречается редко).

Состояния промиса и поток типов: промис начинается в состоянии pending (ожидание) и переходит либо в fulfilled (выполнен, со значением типа T), либо в rejected (отклонён, с причиной типа any). TypeScript отслеживает эти состояния через систему типов, поэтому вы обрабатываете и успешный случай, и ошибку.

Параметр типа в Promise<T> сообщает TypeScript, чем разрешится промис, а это даёт более строгую проверку типов и лучшую поддержку редактора.

Простой пример с Promise

fetchGreeting возвращает Promise<string>, поэтому внутри .then TypeScript знает, что greeting — это строка. Промис случайным образом разрешается или отклоняется, поэтому в выводе будет либо приветствие в верхнем регистре, либо строка с ошибкой.

Prompt
Создайте стрелочную функцию fetchGreeting с типом возвращаемого значения Promise<string>. Внутри постройте Promise, который ждёт одну секунду с помощью setTimeout, а затем случайным образом (через Math.random() > 0.5) либо разрешается значением "Привет, TypeScript!", либо отклоняется с Error("Не удалось получить приветствие"). Используйте его через .then — выводя приветствие в верхнем регистре, поскольку TypeScript знает, что это строка, — и через .catch с ошибкой типа Error, выводя "Ошибка:" и её сообщение. Вывод случаен: либо приветствие в верхнем регистре, либо строка с ошибкой.
Вывод: программа на TypeScript и захваченный вывод

Async/Await в TypeScript

Синтаксис async/await — более понятный способ работы с промисами. Асинхронный код читается как обычный пошаговый код, а TypeScript продолжает проверять типы.

Основные преимущества async/await:

  • Читаемость: последовательный код, за которым проще следить.
  • Обработка ошибок: try/catch подходит и для синхронных, и для асинхронных ошибок.
  • Отладка: трассировки стека выглядят как у синхронного кода.
  • Типобезопасность: полный вывод и проверка типов в TypeScript.

Типы возвращаемых значений async-функций. Каждая async-функция возвращает промис, и объявленный тип возвращаемого значения должен быть обёрнут в него: async function getString(): string { } — это ошибка, а async function getString(): Promise<string> { } — верно.

Примечание: Параметр типа возвращаемого промиса — это тип, который вы пишете после Promise в типе возвращаемого значения.

Простой пример Async/Await

fetchUsers возвращает Promise<User[]>, поэтому после await переменная users имеет тип User[], а обращение к свойствам каждого пользователя проверяется по типам.

Prompt
Определите интерфейс User (id, name, email и role, ограниченное значениями 'admin' | 'user' | 'guest') и async function fetchUsers(): Promise<User[]>, которая выводит 'Загрузка пользователей...', ожидает односекундный setTimeout для имитации вызова API, а затем возвращает Alice (admin) и Bob (user). Напишите async function processUsers(), которая внутри try/catch ожидает fetchUsers(), выводит, сколько пользователей получено, отбирает администраторов и преобразует их в список email, выводит 'Email администраторов:' с этим массивом и повторно выбрасывает любую ошибку после её вывода (с проверкой error instanceof Error). В конце вызовите processUsers() и выведите 'Обработка завершена' при успехе или 'Обработка не удалась:' при неудаче.
Вывод: программа на TypeScript и захваченный вывод

Параллельное выполнение с Promise.all

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

Prompt
Определите интерфейс Product (id, name, price) и async function fetchProduct(id: number): Promise<Product>, которая выводит Загрузка товара <id>..., ждёт случайное время до одной секунды и возвращает товар с именем Товар <id> и случайной целой ценой меньше 100. Напишите fetchMultipleProducts(), которая параллельно запускает fetchProduct(1), (2) и (3) через Promise.all, разбирает три результата, суммирует их цены и выводит Итоговая цена: $<сумма с двумя знаками после запятой>; любую ошибку выводите с 'Ошибка при загрузке товаров:'. Вызовите функцию. Итоговая сумма случайна.
Вывод: программа на TypeScript и захваченный вывод

Типизация колбэков для асинхронных операций

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

Пример

Тип FetchCallback говорит, что колбэк получает Error или null, а также необязательные строковые данные. Внутри колбэка TypeScript знает, что data — это строка или undefined.

Prompt
Определите псевдоним типа FetchCallback для колбэка (error: Error | null, data?: string) => void и функцию fetchDataWithCallback(url: string, callback: FetchCallback): void, которая имитирует асинхронную работу с помощью односекундного setTimeout, вызывая колбэк с null и "Данные ответа" (или с Error, если что-то пойдёт не так). Вызовите её с 'https://api.example.com' и колбэком, который при ошибке выводит 'Ошибка:' и завершается, а иначе выводит данные в верхнем регистре, если они определены.
Вывод: программа на TypeScript и захваченный вывод

Комбинирование промисов

TypeScript даёт типизированные версии методов, которые объединяют несколько промисов. Они помогают выполнять операции одновременно и безопасно обрабатывать результаты.

Методы комбинирования промисов:

  • Promise.all() — ждёт разрешения всех промисов.
  • Promise.race() — возвращает первый завершившийся промис.
  • Promise.allSettled() — ждёт завершения всех (успешного или неудачного).
  • Promise.any() — возвращает первый успешно выполненный промис.

Внимание: Когда вы передаёте в Promise.all промисы разных типов, TypeScript выводит кортеж из отдельных типов результатов. Если выведенный тип недостаточно точен, используйте утверждение типа или сами опишите ожидаемую структуру.

Promise.all — параллельное выполнение

Запускает несколько промисов параллельно и ждёт их все. Завершается сразу с ошибкой, если хотя бы один промис отклонён. Здесь user, posts и stats получают собственные точные типы из результата-кортежа.

Prompt
Создайте три типизированные стрелочные функции, возвращающие уже разрешённые Promise: fetchUser(id) даёт { id, name: "Пользователь <id>" }, fetchPosts(userId) даёт две записи (Запись 1, Запись 2) с идентификаторами, а fetchStats(userId) даёт { views: 100, likes: 25 }. Напишите async function loadUserDashboard(userId: number), которая ожидает все три через Promise.all, выводит Пользователь: <name>, Записей: <count> и Лайков: <likes>, возвращает три значения в объекте и выводит 'Не удалось загрузить панель:' перед повторным выбросом ошибки. Вызовите её с идентификатором пользователя 1.
Вывод: программа на TypeScript и захваченный вывод

Promise.race — первый завершившийся

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

Prompt
Напишите вспомогательную функцию timeout(ms: number): Promise<never>, которая через ms миллисекунд отклоняется с Error("Тайм-аут через <ms> мс"), и обобщённую async function fetchWithTimeout<T>(promise: Promise<T>, timeoutMs = 5000): Promise<T>, которая состязает переданный promise с тайм-аутом и выбрасывает Время ожидания запроса истекло через <ms> мс, если побеждает тайм-аут. Затем определите (но не вызывайте) fetchUserData(), которая применяет fetchWithTimeout к fetch('https://api.example.com/user/1') с ограничением в 3 секунды, разбирает JSON, а при ошибке выводит 'Ошибка:' с сообщением (приведя к Error) и повторно выбрасывает её. Ничего не выводится, потому что ни одна функция не вызывается.
Вывод: программа на TypeScript и захваченный вывод

Promise.allSettled — обработка всех результатов

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

Prompt
Создайте асинхронную функцию fetchData(id: number), которая выбрасывает Error("Не удалось получить данные для ID <id>"), когда Math.random() > 0.7, и иначе возвращает { id, data: "Данные для <id>" }. Напишите async function processBatch(ids: number[]), которая преобразует каждый id в fetchData(id), обёрнутый так, чтобы он разрешался значением { status: 'fulfilled' as const, value } или { status: 'rejected' as const, reason }, ожидает Promise.allSettled для них, разделяет успешные и неудачные результаты фильтрами с предикатами типа (PromiseFulfilledResult / PromiseRejectedResult), выводит Успешно обработано: <n> и Не удалось: <n> и возвращает оба списка. Вызовите её с идентификаторами от 1 до 5. Количества случайны.
Вывод: программа на TypeScript и захваченный вывод

Обработка ошибок в асинхронном коде

TypeScript даёт хорошие инструменты для типобезопасной обработки ошибок в асинхронном коде. Вот некоторые приёмы и рекомендации.

Стратегии обработки ошибок:

  • Блоки try/catch: для обработки ошибок в async/await.
  • Границы ошибок: для компонентов React.
  • Типы результата: функциональный подход со значениями успеха или неудачи.
  • Наследование классов ошибок: для ошибок конкретной предметной области.

Приёмы обработки ошибок, которые стоит рассмотреть:

  1. Границы ошибок в React для обработки на уровне компонента.
  2. Объекты результата вместо выбрасывания исключений для ожидаемых случаев.
  3. Глобальный обработчик ошибок для неперехваченных исключений.
  4. Логирование ошибок, чтобы фиксировать и сообщать об ошибках.

Пользовательские классы ошибок

Создавайте типы ошибок для своей предметной области, чтобы обрабатывать их точнее. AppError — базовый класс, а NetworkError, ValidationError и NotFoundError несут каждый свой код. Локальная mockFetch заменяет fetch, чтобы пример работал без сети.

Prompt
Постройте небольшую иерархию ошибок: базовый класс AppError extends Error, конструктор которого принимает message, public readonly code: string и необязательное public readonly details?: unknown, задаёт this.name равным имени класса и, если возможно, захватывает трассировку стека; затем NetworkError (код 'NETWORK_ERROR'), ValidationError (код 'VALIDATION_ERROR', с public readonly field) и NotFoundError (код 'NOT_FOUND', сообщение <resource> с ID <id> не найден). Напишите async function fetchUserData(userId: string): Promise<{ id: string; name: string }>, которая ожидает локальную mockFetch (Promise, разрешающийся поддельным ответом { ok, status, json() }, чтобы сеть не использовалась), выбрасывает NotFoundError при 404, NetworkError при 5xx, обычную Error при других неуспешных статусах и ValidationError('name', ...), когда в JSON нет name; непредвиденные ошибки оборачиваются в AppError с кодом 'UNEXPECTED_ERROR'. Добавьте displayUserProfile(userId), которая выводит 'Профиль пользователя:' вместе с пользователем либо выводит через console.error свой префикс для каждого класса ошибки (Проблема с сетью:, Проверка не пройдена:, Не найдено:, Непредвиденная ошибка:), и вызовите её с '123'.
Вывод: программа на TypeScript и захваченный вывод

Асинхронная итерация в TypeScript

TypeScript поддерживает асинхронные итераторы и асинхронные генераторы с полной типизацией. Цикл for await достаёт значения по одному, как только они готовы.

Пример

generateNumbers типизирована как AsyncGenerator<number, void, unknown>, поэтому внутри цикла for await num — это число. Здесь функции только определены, поэтому ничего не выводится.

Prompt
Напишите асинхронный генератор generateNumbers() с типом AsyncGenerator<number, void, unknown>, который выдаёт значения от 0 до 4, ожидая одну секунду (через setTimeout внутри Promise с await) перед каждым значением. Добавьте async function consumeNumbers(), которая перебирает генератор с помощью for await и выводит каждое число, умноженное на два. Только определите функции; ничего не вызывается, поэтому ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод