Асинхронное программирование в TypeScript
Дальше: Дженерики · TypeScript и Node.js · Обработка ошибок · Справочник — расширенные типы
Механизм async в TypeScript тот же, что в JavaScript: Promise, event loop, микрозадачи. TypeScript добавляет контракты — что лежит внутри Promise, какие поля у ответа API, как описать состояния UI так, чтобы компилятор видел все ветки.
Маршрут: Типы и типизация → функции → async → TypeScript и React / TypeScript и Node.js.
Сначала: асинхронность в JavaScript — очереди,
Promise,async/await.
Что даёт типизация async-кода
| Без типов | С TypeScript |
|---|---|
res.json() — any | Явный User или unknown + парсер |
| Забыли поле в ответе | Ошибка при обращении к user.email |
Смешали loading и data | Discriminated union по status |
catch (e) — неясно что в e | catch (error: unknown) + сужение |
Проверка не отменяет сетевые сбои: типы описывают ожидания, валидацию внешних JSON — рекомендации (Zod и др.).
Promise<T>
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.
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) — см. 21.md.
Типизация fetch и JSON
Ответ сети не гарантирует форму данных. Опасный путь — слепое приведение:
// Плохо: компилятор верит, сервер может прислать что угодно
const user = (await res.json()) as User;
Учебный контракт и безопасный разбор:
interface User {
id: string;
name: string;
}
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) return false;
const o = value as Record<string, unknown>;
return typeof o.id === "string" && typeof o.name === "string";
}
async function loadUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const raw: unknown = await res.json();
if (!isUser(raw)) {
throw new Error("Неверный формат ответа");
}
return raw;
}
Разбор:
unknownпослеjson()честно отражает, что структура ответа ещё не проверена.- Type predicate
value is Userсужает тип после проверки. - В production часто используют Zod / valibot — 6.md.
Состояние загрузки как discriminated union
Один из самых практичных паттернов UI и сервисов — объединение состояний с полем-дискриминантом (подробнее в 10.md):
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:
function renderUserState(state: UserLoadState): string {
switch (state.status) {
case "idle":
return 'Нажмите "Загрузить"';
case "loading":
return "Загрузка…";
case "success":
return state.data.name;
case "error":
return state.message;
default: {
const _exhaustive: never = state;
return _exhaustive;
}
}
}
Разбор:
- В ветке
successполеdataдоступно — TS знает это поstatus. neverвdefaultловит забытый вариант union при расширении типа.- Отдельные флаги
isLoading+data?часто дают невозможные комбинации; union этого не допускает.
Параллельные запросы: Promise.all
type Dashboard = {
user: User;
notifications: { id: string; text: string }[];
};
async function loadDashboard(userId: string): Promise<Dashboard> {
const [user, notifications] = await Promise.all([
loadUser(userId),
loadNotifications(userId),
]);
return { user, notifications };
}
async function loadNotifications(
userId: string,
): Promise<{ id: string; text: string }[]> {
const res = await fetch(`/api/notifications/${userId}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const raw: unknown = await res.json();
if (!Array.isArray(raw)) throw new Error("Ожидался массив");
return raw as { id: string; text: string }[];
}
Разбор:
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сохраняется у успешного результата — см. 24.md.
Обработка ошибок: unknown в catch
function toMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
return "Неизвестная ошибка";
}
async function safeLoad(id: string): Promise<User | null> {
try {
return await loadUser(id);
} catch (error: unknown) {
console.error(toMessage(error));
return null;
}
}
Разбор:
- В modern TS в
catchлучшеunknown, неany. instanceof Error— частый случай; произвольныеthrow "строка"тоже встречаются в JS.
Не используйте catch (e: any) как shortcut — в strict-проекте это обнуляет пользу типов.
Retry для нестабильных API
async function withRetry<T>(
task: () => Promise<T>,
retries = 2,
): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt <= retries; attempt += 1) {
try {
return await task();
} catch (error: unknown) {
lastError = error;
}
}
throw new Error(toMessage(lastError));
}
// Использование
const user = await withRetry(() => loadUser("u-1"), 3);
Разбор:
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 vs Node) — дженерики §infer.- Для полей класса, общих утилит и библиотек не хардкодьте
numberбез проверки целевой среды. - Отмена:
clearTimeout(timerId)в обеих средах; в Node дляsetInterval— тот же приём.
Async в callback-API
Иногда библиотека ожидает (err, data) => void. Типизируйте обёртку:
function readFileUtf8(path: string): Promise<string> {
return new Promise((resolve, reject) => {
// псевдокод Node callback API
fakeRead(path, (err: Error | null, data?: string) => {
if (err) reject(err);
else resolve(data ?? "");
});
});
}
declare function fakeRead(
path: string,
cb: (err: Error | null, data?: string) => void,
): void;
В Node 20+ предпочитайте fs/promises с уже типизированными Promise — 22.md.
Связь с event loop
TypeScript не меняет порядок микрозадач и макрозадач. Типы не помогут, если вы ожидаете синхронный код после await вне async-функции.
| Концепция JS | Напоминание для TS |
|---|---|
async → всегда Promise | аннотируйте возврат Promise<T> явно при экспорте API |
await → микрозадача | порядок логов как в 21.md |
| 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 падает | сеть/логика | тесты, валидация, 27.md |
Практика
- Опишите
LoadState<Product>и функциюloadProduct(id)с ветками success/error. - Напишите
render(state)сdefault: neverдля exhaustive check. - Реализуйте
loadDashboardчерезPromise.allс двумя разными endpoint. - Оберните загрузку в
withRetry(3 попытки) и залогируйтеtoMessageпри провале. - Замените
as UserнаisUserpredicate и убедитесь, что ложный JSON не проходит типизацию.
Смежные статьи
- Типы — discriminated union,
never, narrowing - Функции — типы callback, возврат
Promise - Дженерики —
Promise<T>,Awaited - Обработка ошибок — Result, кастомные ошибки
- TypeScript и Node.js —
fs/promises, HTTP-сервер - TypeScript и React — хуки и загрузка данных
- Справочник — 301
- Event loop: JS 21