Асинхронное программирование в TypeScript
Дальше: Дженерики · TypeScript и Node.js · Обработка ошибок · Справочник — расширенные типы
Механизм async в TypeScript тот же, что в JavaScript — Promise, event loop, микрозадачи. TypeScript добавляет контракты — что лежит внутри Promise, какие поля у ответа API, как описать состояния UI так, чтобы компилятор видел все ветки. Удобный порядок — типы и типизация, функции, эта статья, затем TypeScript и React или TypeScript и Node.js.
Сначала — асинхронность в JavaScript — очереди,
Promise,async/await.
Что даёт типизация async-кода
Play ITЗагрузка интерактивного демо…
| Без типов | С TypeScript |
|---|---|
res.json() — any | Явный User или unknown + парсер |
| Забыли поле в ответе | Ошибка при обращении к user.email |
Смешали loading и data | Discriminated union по status |
catch (e) — неясно что в e | catch (error: unknown) + сужение |
Проверка не отменяет сетевые сбои: типы описывают ожидания, валидацию внешних JSON — рекомендации (Zod и др.).
Promise<T>
Play ITЗагрузка интерактивного демо…
Promise — обобщённый тип: параметр T — тип успешного результата.
function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function fetchNumber(): Promise<number> {
return Promise.resolve(42);
}
Разбор:
Promise<void>— успех без полезного значения (аналог пустогоreturn).Promise<number>— приawaitполучитеnumber.- Отклонение (
reject) типом не параметризуется в стандартной библиотеке — вcatchработают сunknown.
Promise.withResolvers<T>()
В JavaScript (ES2024) — см. § Управляемый Promise. В TypeScript метод обобщён: T — тип успешного результата.
function waitForSignal(): Promise<string> {
const { promise, resolve, reject } = Promise.withResolvers<string>();
socket.once("open", () => resolve("connected"));
socket.once("error", (err) => reject(err));
return promise;
}
Разбор:
promiseимеет типPromise<string>; послеawait—string.resolveпринимаетT | PromiseLike<T>;reject—reason?: unknown(как у обычногоPromise).- Паттерн с
let resolveвне executor'а в TS часто даётanyуresolve;withResolversсохраняет связь сT.
Требуется lib с ES2024 (например "lib": ["ES2024"] или новее в tsconfig) и среда выполнения с поддержкой метода.
async и await
async-функция всегда возвращает Promise:
async function loadLabel(): Promise<string> {
await delay(100);
return "готово";
}
// Эквивалентно: Promise<string>
const p: Promise<string> = loadLabel();
Разбор:
return "готово"внутриasyncоборачивается вPromise.resolve.awaitразворачиваетPromise<T>вTвнутри async-функции.- Если
awaitполучает не-Promise, значение оборачивается (как в JS) — см. Асинхронное программирование в JavaScript.
Типизация fetch и JSON
Ответ сети не гарантирует форму данных. Опасный путь — слепое приведение:
// Плохо: компилятор верит, сервер может прислать что угодно
const user = (await res.json()) as User;
Учебный контракт и безопасный разбор:
Код ITЗагрузка примера кода…
Разбор:
unknownпослеjson()честно отражает, что структура ответа ещё не проверена.- Type predicate
value is Userсужает тип после проверки. - В production часто используют Zod / valibot — Рекомендации по разработке на TypeScript.
Состояние загрузки как discriminated union
Один из самых практичных паттернов UI и сервисов — объединение состояний с полем-дискриминантом (подробнее в Типы данных и типизация в TypeScript):
type LoadState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; message: string };
type User = { id: string; name: string };
type UserLoadState = LoadState<User>;
Функция загрузки описывает весь жизненный цикл запроса (idle, loading, success, error), а не один лишь тип User:
async function fetchUserState(id: string): Promise<UserLoadState> {
try {
const user = await loadUser(id);
return { status: "success", data: user };
} catch (error: unknown) {
return { status: "error", message: toMessage(error) };
}
}
Отрисовка с исчерпывающим switch:
Код ITЗагрузка примера кода…
Разбор:
- В ветке
successполеdataдоступно — TS знает это поstatus. neverвdefaultловит забытый вариант union при расширении типа.- Отдельные флаги
isLoading+data?часто дают невозможные комбинации; union этого не допускает.
Параллельные запросы — Promise.all
Код ITЗагрузка примера кода…
Разбор:
Promise.allвозвращает кортеж типов[A, B, …]при фиксированном списке промисов.- Если один промис rejected — весь
allrejected (для частичного успеха —Promise.allSettled).
Тип Promise.allSettled:
const results = await Promise.allSettled([
loadUser("1"),
loadUser("2"),
]);
for (const r of results) {
if (r.status === "fulfilled") {
console.log(r.value.name);
} else {
console.log(r.reason);
}
}
Promise.race и таймаут
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("Таймаут")), ms);
});
return Promise.race([promise, timeout]);
}
Разбор:
Promise<never>у таймаута — ветка только сreject.- Дженерик
Tсохраняется у успешного результата — см. Дженерики в TypeScript.
Обработка ошибок — unknown в catch
Код ITЗагрузка примера кода…
Разбор:
- В modern TS в
catchпредпочтительнееunknownвместоany. instanceof Error— частый случай; произвольныеthrow "строка"тоже встречаются в JS.
Не используйте catch (e: any) как shortcut — в strict-проекте это обнуляет пользу типов.
Retry для нестабильных API
Код ITЗагрузка примера кода…
Разбор:
task— фабрика промиса: каждая попытка создаёт новый запрос.- Тип
Tпробрасывается сквозь retry без потери информации.
Тип Awaited<T>
Встроенный utility извлекает тип результата Promise (в том числе вложенного):
type UserPromise = Promise<User>;
type ResolvedUser = Awaited<UserPromise>; // User
type Nested = Awaited<Promise<Promise<number>>>; // number
Полезно в обёртках над fetch и в generic-хелперах — Справочник — утилитарные типы.
Таймеры — setTimeout в браузере и Node
Многие ожидают, что setTimeout всегда возвращает число. В браузере так и есть; в Node.js — объект Timeout.
const id = setTimeout(() => {}, 1000);
// В браузере: typeof id === "number"
// В Node.js: typeof id === "object"
Из-за этого универсальный fullstack-код с let timerId: number ломается в одной из сред:
// let timerId: number;
// timerId = setTimeout(fn, 1000); // ошибка в Node или в DOM typings
let timerId: ReturnType<typeof setTimeout>;
timerId = setTimeout(() => {}, 1000);
clearTimeout(timerId);
Разбор:
ReturnType<typeof setTimeout>подставляет фактический тип из текущих@types(DOM или Node) — дженерики §infer.- Для полей класса, общих утилит и библиотек не хардкодьте
numberбез проверки целевой среды. - Отмена:
clearTimeout(timerId)в обеих средах; в Node дляsetInterval— тот же приём.
Async в callback-API
Иногда библиотека ожидает (err, data) => void. Типизируйте обёртку:
Код ITЗагрузка примера кода…
В Node 20+ предпочитайте fs/promises с уже типизированными Promise — TypeScript и Node.js.
Связь с event loop
TypeScript не меняет порядок микрозадач и макрозадач. Типы не помогут, если вы ожидаете синхронный код после await вне async-функции.
| Концепция JS | Напоминание для TS |
|---|---|
async → всегда Promise | аннотируйте возврат Promise<T> явно при экспорте API |
await → микрозадача | порядок логов как в Асинхронное программирование в JavaScript |
| Unhandled rejection | обрабатывайте catch или .catch() |
Частые ошибки
| Ошибка | Причина | Что делать |
|---|---|---|
as User на json() | ложная уверенность | unknown + guard или Zod |
async без await | лишний Promise в стеке | убрать async или добавить реальное ожидание |
Забыли await | получили Promise вместо T | включить ESLint @typescript-eslint/no-floating-promises |
isLoading + data отдельно | несогласованные состояния | LoadState union |
catch (e: any) | отключена проверка | unknown + toMessage |
let id: number = setTimeout(...) | разные типы DOM и Node | ReturnType<typeof setTimeout> |
| Типы в порядке, runtime падает | сеть/логика | тесты, валидация, Обработка ошибок в TypeScript |
Практика
- Опишите
LoadState<Product>и функциюloadProduct(id)с ветками success/error. - Напишите
render(state)сdefault: neverдля exhaustive check. - Реализуйте
loadDashboardчерезPromise.allс двумя разными endpoint. - Оберните загрузку в
withRetry(3 попытки) и залогируйтеtoMessageпри провале. - Замените
as UserнаisUserpredicate и убедитесь, что ложный JSON не проходит типизацию.
Практикум — параллельное выполнение
TypeScript использует ту же модель, что JavaScript. Отличие — типы у Promise и у результата Promise.all.
Типизированный Promise.all
type Page = { url: string; delayMs: number };
async function loadAll(pages: Page[]): Promise<string[]> {
const start = performance.now();
const results = await Promise.all(
pages.map(async (p) => {
await new Promise((r) => setTimeout(r, p.delayMs));
return `OK ${p.url}`;
}),
);
console.log(`Параллельно: ${((performance.now() - start) / 1000).toFixed(2)} с`);
return results;
}
Promise<string[]>— послеawaitвresultsлежит массив строк, а неany;Pageописывает поля учебной "страницы";- ошибки в
catch— сunknown, см. Обработка ошибок.
Последовательный вариант для сравнения
async function loadSequential(pages: Page[]): Promise<string[]> {
const results: string[] = [];
for (const p of pages) {
await new Promise((r) => setTimeout(r, p.delayMs));
results.push(`OK ${p.url}`);
}
return results;
}
Сравнение языков — практикум раздела "Асинхронность". Event loop — JavaScript 21. Promise<T> и Awaited — Дженерики.
Смежные статьи
- Типы — discriminated union,
never, narrowing - Функции — типы callback, возврат
Promise - Дженерики —
Promise<T>,Awaited - Обработка ошибок — Result, кастомные ошибки
- TypeScript и Node.js —
fs/promises, HTTP-сервер - TypeScript и React — хуки и загрузка данных
- Справочник — Справочник по TypeScript
- Event loop: JS 21