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

Асинхронное программирование в 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 и dataDiscriminated union по status
catch (e) — неясно что в ecatch (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>; после awaitstring.
  • resolve принимает T | PromiseLike<T>; rejectreason?: 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 — весь all rejected (для частичного успеха — 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 с уже типизированными PromiseTypeScript и 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 и NodeReturnType<typeof setTimeout>
Типы в порядке, runtime падаетсеть/логикатесты, валидация, Обработка ошибок в TypeScript

Практика

  1. Опишите LoadState<Product> и функцию loadProduct(id) с ветками success/error.
  2. Напишите render(state) с default: never для exhaustive check.
  3. Реализуйте loadDashboard через Promise.all с двумя разными endpoint.
  4. Оберните загрузку в withRetry (3 попытки) и залогируйте toMessage при провале.
  5. Замените as User на isUser predicate и убедитесь, что ложный 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Дженерики.


Смежные статьи