$ sudo teach IT

МОДУЛЬ 10 · УРОК 2

Async/Await с типами: Полное руководство

Асинхронное программирование с типобезопасностью в TypeScript

Содержание

  1. Введение в async/await с типами
  2. Тип возвращаемого значения async-функции (Promise<T>)
  3. Автоматическое выведение типов в async
  4. Явная аннотация возвращаемого типа
  5. Async-стрелочные функции с типами
  6. Await и типизация промисов
  7. Цепочки типов в асинхронных операциях
  8. Async-методы в классах
  9. Async-методы в интерфейсах
  10. Последовательный await vs Параллельный await
  11. Promise.all с типами (кортежи)
  12. Promise.race и Promise.allSettled с типами
  13. Обработка ошибок с типами (try/catch)
  14. Типизация ошибок: unknown vs any
  15. Async-итераторы (AsyncIterable<T>)
  16. Async-генераторы
  17. Typed retry паттерн
  18. Typed timeout паттерн
  19. Typed throttle/debounce
  20. Async middleware typing
  21. Async/await с generic-функциями
  22. Типизация async callback-хелперов
  23. Реальные примеры: API клиент
  24. Реальные примеры: Загрузка данных
  25. Типичные ошибки при работе с async/await
  26. Лучшие практики и советы

1. Введение в async/await с типами

Асинхронное программирование является одной из фундаментальных концепций современной веб-разработки. В JavaScript асинхронные операции традиционно обрабатывались с помощью колбэков и промисов (Promise). Однако с введением ключевых слов async и await в ES2017, асинхронный код стал гораздо более читаемым и удобным для написания.

TypeScript значительно расширяет возможности асинхронного программирования, добавляя строгую типизацию к каждому аспекту async/await. Это означает, что типы возвращаемых значений промисов, типы ошибок и типы параметров — всё это проверяется на этапе компиляции, что значительно снижает количество runtime-ошибок.

Ключевой момент, который важно понимать: любая функция, объявленная с ключевым словом async, всегда возвращает Promise. Даже если вы явно возвращаете обычное значение, TypeScript автоматически обернёт его в Promise. Это критически важно для понимания системы типов.

// Базовый пример async/await с типами
async function fetchUserName(userId: number): Promise<string> {
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();
  return data.name as string;
}

const name: string = await fetchUserName(42);
// name имеет тип string — TypeScript знает это!
Важно: В контексте модуля верхнего уровня (top-level await) или в блоке async-функции, выражение с await автоматически возвращает значение, извлечённое из Promise, а не сам Promise. TypeScript понимает эту семантику и соответствующим образом выводит типы.
💡 Совет учителя: Начните с объяснения разницы между callback-подходом и async/await. Покажите, как TypeScript обеспечивает типобезопасность на каждом шаге. Студенты должны понять, что async/await — это не просто синтаксический сахар, а полноценный механизм с мощной системой типов.

2. Тип возвращаемого значения async-функции (Promise<T>)

Когда вы объявляете функцию как async, TypeScript автоматически оборачивает возвращаемый тип в Promise. Это означает, что если функция возвращает строку, её реальный тип будет Promise<string>. Это не просто синтаксический сахар — это фундаментальное правило типизации, которое влияет на то, как вы взаимодействуете с результатами асинхронных вызовов.

Существует несколько способов объявить тип возвращаемого значения async-функции. Вы можете явно указать тип возвращаемого значения как Promise<T>, или TypeScript может автоматически вывести его на основе оператора return. Оба подхода имеют свои особенности, и понимание их различий критически важно для написания качественного кода.

Следует обратить внимание на то, что когда вы используете оператор return внутри async-функции, вы можете вернуть как Promise, так и обычное значение. В обоих случаях функция будет возвращать Promise. Если вы вернёте Promise, он будет "развёрнут" (awaited) автоматически, что позволяет писать более элегантный код.

// Явное указание Promise<T> как возвращаемого типа
async function getData(): Promise<{ id: number; name: string }> {
  return { id: 1, name: 'Alice' };
}

// Возвращаем Promise напрямую — тоже валидно
async function getDataWrapped(): Promise<{ id: number; name: string }> {
  const data = { id: 1, name: 'Alice' };
  return Promise.resolve(data);
}

// Автоматическое выведение: TypeScript понимает тип
async function autoInfer() {
  return { id: 1, name: 'Alice' };
}
// autoInfer возвращает Promise<{ id: number; name: string }>

const result1 = await getData();
const result2 = await getDataWrapped();
const result3 = await autoInfer();
// Все три переменные имеют тип { id: number; name: string }
Информация: TypeScript 4.5 и выше поддерживают Promise<Awaited<T>> для более точного выведения типов при вложенных промисах. Это означает, что если async-функция возвращает Promise<Promise<string>>, TypeScript автоматически "развернёт" его до Promise<string>.
💡 Совет учителя: Объясните студентам разницу между Promise<string> и string. Когда функция объявлена как async, она всегда возвращает Promise, даже если в коде написано return "hello". TypeScript автоматически оборачивает тип. Покажите примеры, где это может быть неочевидно.

3. Автоматическое выведение типов в async

TypeScript обладает мощной системой автоматического вывода типов, которая работает и в контексте async-функций. Когда вы не указываете явно возвращаемый тип, анализатор типов TypeScript автоматически определяет его на основе операторов return в функции. Это делает код более лаконичным, сохраняя при этом полную типобезопасность.

Однако автоматическое выведение не всегда работает идеально. В частности, если функция содержит несколько операторов return с различными типами, TypeScript может вывести объединение (union) всех возможных типов. В таких случаях лучше указывать возвращаемый тип явно, чтобы избежать неожиданного поведения.

Ещё одна важная особенность — это выведение типов при использовании await. Когда вы присваиваете результат await переменной, TypeScript определяет тип этой переменной на основе типа значения, которое Promise возвращает после разрешения. Это позволяет безопасно использовать результат асинхронной операции без необходимости вручную указывать его тип.

// Автоматическое выведение возвращаемого типа
async function fetchUser(id: number) {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return data as { id: number; name: string; email: string };
}
// TypeScript выведет: Promise<{ id: number; name: string; email: string }>

// Вывод типов при присваивании
const user = await fetchUser(1);
// user: { id: number; name: string; email: string }
// TypeScript автоматически вывел тип!

// Несколько return — выведение union-типа
async function getStatus(code: number) {
  if (code === 200) return 'success';
  if (code === 404) return 'not found';
  return null;
}
// TypeScript выведет: Promise<string | null>
Предупреждение: Автоматическое выведение типов работает хорошо для простых случаев, но для сложных функций с несколькими ветвями выполнения лучше всегда указывать возвращаемый тип явно. Это делает код более предсказуемым и помогает ловить ошибки на ранних этапах.

4. Явная аннотация возвращаемого типа

Хотя TypeScript умеет автоматически выводить типы, явное указание возвращаемого типа — это рекомендуемая практика для публичных функций и методов. Явная аннотация выполняет сразу несколько важных функций: она служит документацией, обеспечивает единообразие интерфейса и помогает ловить ошибки на этапе компиляции.

Когда вы указываете возвращаемыйтип явно, TypeScript проверяет, что функция действительно возвращает значение указанного типа. Если тело функции не соответствует объявленному типу, компилятор выдаст ошибку. Это особенно полезно при рефакторинге, когда изменения в одной части кода могут повлиять на типы в другой.

Для async-функций важно помнить, что вы указываете тип значения, которое будет получено после await, а не тип самого Promise. TypeScript автоматически оборачивает указанный тип в Promise.

// Явная аннотация возвращаемого типа
async function fetchProducts(): Promise<Product[]> {
  const response = await fetch('/api/products');
  return response.json();
}

interface Product {
  id: number;
  name: string;
  price: number;
}

// Явная аннотация с union-типом
async function findUser(id: number): Promise<User | null> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) return null;
  return response.json();
}

// Явная аннотация помогает ловить ошибки
async function getCount(): Promise<number> {
  const data = await fetchData();
  return data.count; // Ошибка, если count не number!
}
Совет: Используйте явную аннотацию возвращаемого типа для всех публичных функций и методов. Это особенно важно для функций, которые экспортируются из модуля. Для внутренних функций, где типы очевидны из контекста, автоматическое выведение допустимо.

5. Async-стрелочные функции с типами

Стрелочные функции (arrow functions) широко используются в современном JavaScript и TypeScript. Когда стрелочная функция помечена ключевым словом async, она возвращает Promise, точно так же как и обычные async-функции. Типизация async-стрелочных функций имеет свои особенности, связанные с синтаксисом и контекстом выполнения.

Одна из ключевых особенностей async-стрелочных функций — это то, как они обрабатывают контекст this. В отличие от обычных функций, стрелочные функции не имеют собственного контекста this и наследуют его из окружающего кода. Это важно учитывать при работе с классами и объектами.

Async-стрелочные функции идеально подходят для callback-стиля программирования, например, при обработке результатов Promise или при передаче асинхронных функций в качестве аргументов. TypeScript обеспечивает полную типизацию для таких конструкций.

// Async-стрелочная функция с типами
const fetchData: (url: string) => Promise<any> = async (url) => {
  const response = await fetch(url);
  return response.json();
};

// Автоматическое выведение типов
const getUser = async (id: number) => {
  return { id, name: 'User ' + id };
};
// Вывод: (id: number) => Promise<{ id: number; name: string }>

// Async-стрелочная функция в цепочке
const result = await fetchData('/api/data');

// Типизация callback-аргументов
const processItems = async (
  items: string[],
  processor: (item: string) => Promise<number>
) => {
  const results = await Promise.all(
    items.map(processor)
  );
  return results;
};

const counts = await processItems(
  ['hello', 'world'],
  async (item) => item.length
);
// counts: number[]
💡 Совет учителя: Покажите разницу между контекстом this в стрелочных и обычных функциях. В async-стрелочных функциях this наследуется из внешнего контекста, что делает их безопасными для использования в обработчиках событий и колбэках.

6. Await и типизация промисов

Оператор await используется для "приостановки" выполнения async-функции до тех пор, пока Promise не будет разрешён. TypeScript типизирует результат await на основе типа значения, которое Promise возвращает после разрешения. Это означает, что вы можете безопасно использовать результат асинхронной операции без необходимости ручного приведения типов.

Однако существует важный нюанс: если вы используете await с выражением, которое не является Promise, TypeScript всё равно будет работать корректно. Await просто вернёт значение как есть. Это полезно для написания кода, который может работать как с синхронными, так и с асинхронными значениями.

Для обработки ошибок при использовании await используется конструкция try/catch. TypeScript позволяет типизировать ошибки, что помогает в обработке различных типов ошибок. Это особенно важно при работе с внешними API, которые могут возвращать различные типы ошибок.

// Await с типизированными промисами
interface ApiResponse {
  data: UserData[];
  status: number;
}

interface UserData {
  id: number;
  name: string;
}

async function fetchUsers(): Promise<UserData[]> {
  const response = await fetch('/api/users');
  const apiResponse: ApiResponse = await response.json();
  return apiResponse.data;
}

// Await с не-Promise значением
async function process() {
  const value = await 42;
  // value: number (не Promise)
  // await просто возвращает значение как есть
}
Полезно знать: В TypeScript 4.5+ была добавлена утилита Awaited<T>, которая позволяет корректно извлекать тип значения из вложенных Promise. Например, Awaited<Promise<Promise<string>>> вернёт string.

7. Цепочки типов в асинхронных операциях

При работе с асинхронными операциями часто возникает необходимость в цепочке вызовов, где результат одной операции передаётся в качестве аргумента другой. TypeScript позволяет создавать сложные цепочки типов, сохраняя типобезопасность на каждом шаге. Это делает возможным композицию асинхронных функций без потери информации о типах.

Цепочки типов особенно полезны при работе с API, где структура данных может меняться на каждом этапе обработки. TypeScript гарантирует, что типы корректно проходят через всю цепочку, что позволяет ловить ошибки на ранних этапах.

Для создания цепочек типов можно использовать generics, conditional types и инференцию типов. Это позволяет создавать функции, которые автоматически выводят типы на основе входных данных, обеспечивая максимальную гибкость и типобезопасность.

// Цепочка асинхронных операций с типами
interface RawUser {
  id: number;
  first_name: string;
  last_name: string;
}

interface ProcessedUser {
  id: number;
  fullName: string;
  displayName: string;
}

async function fetchRawUsers(): Promise<RawUser[]> {
  const res = await fetch('/api/raw-users');
  return res.json();
}

async function processUsers(rawUsers: RawUser[]): Promise<ProcessedUser[]> {
  return rawUsers.map(user => ({
    id: user.id,
    fullName: `${user.first_name} ${user.last_name}`,
    displayName: `${user.first_name} ${user.last_name.charAt(0)}.`
  }));
}

// Цепочка с полной типизацией
const users = await processUsers(await fetchRawUsers());
// users: ProcessedUser[]

// Или с промежуточными переменными
const rawUsers = await fetchRawUsers();
// rawUsers: RawUser[]
const processedUsers = await processUsers(rawUsers);
// processedUsers: ProcessedUser[]
Важно: При создании цепочек убедитесь, что типы промежуточных результатов корректно определены. TypeScript может вывести сложные типы, но иногда лучше явно аннотировать промежуточные переменные для улучшения читаемости кода и документации.

8. Async-методы в классах

В TypeScript классы могут содержать async-методы, которые возвращают Promise. Это широко используется при организации кода, связанного с асинхронными операциями, такими как работа с базой данных, файловой системой или внешними API. Типизация async-методов в классах позволяет создавать строго типизированные API и обеспечивать безопасность типов при вызове этих методов.

При объявлении async-метода в классе вы можете указать возвращаемый тип явно, что является рекомендуемой практикой. Это не только документирует контракт метода, но и позволяет TypeScript проверять, что все ветви выполнения возвращают значение правильного типа.

Важно отметить, что вызов async-метода класса всегда возвращает Promise, даже если метод не содержит операторов await. Это означает, что код, вызывающий такой метод, должен либо использовать await, либо работать с Promise напрямую.

// Класс с async-методами
class UserRepository {
  private db: Database;

  constructor(db: Database) {
    this.db = db;
  }

  async findById(id: number): Promise<User | null> {
    const result = await this.db.query('SELECT * FROM users WHERE id = ?', [id]);
    return result.rows[0] || null;
  }

  async findAll(): Promise<User[]> {
    const result = await this.db.query('SELECT * FROM users');
    return result.rows;
  }

  async create(userData: CreateUserDTO): Promise<User> {
    const result = await this.db.query(
      'INSERT INTO users (name, email) VALUES (?, ?) RETURNING *',
      [userData.name, userData.email]
    );
    return result.rows[0];
  }

  async delete(id: number): Promise<boolean> {
    const result = await this.db.query('DELETE FROM users WHERE id = ?', [id]);
    return result.rowCount > 0;
  }
}

// Использование
const repo = new UserRepository(database);
const user = await repo.findById(1);
// user: User | null
💡 Совет учителя: Объясните студентам паттерн "Repository", который широко используется для работы с базами данных. Покажите, как TypeScript позволяет создавать строго типизированные интерфейсы для CRUD-операций. Это поможет студентам понять ценность типизации в реальных проектах.

9. Async-методы в интерфейсах

Интерфейсы в TypeScript позволяют определять контракты для классов и объектов. Когда интерфейс содержит async-метод, он определяет, что реализующий класс должен предоставить метод, возвращающий Promise определённого типа. Это создаёт строго типизированные контракты для асинхронных операций.

Одна из ключевых особенностей работы с async-методами в интерфейсах — это то, что интерфейс определяет только сигнатуру метода, а не его реализацию. Реализующий класс может использовать async/await или возвращать Promise напрямую — оба варианта будут удовлетворять интерфейсу.

Async-интерфейсы особенно полезны при проектировании приложений с использованием принципа Dependency Injection (Внедрение Зависимости). Они позволяют определять контракты для сервисов, которые могут иметь различные реализации (HTTP-клиент, клиент базы данных, файловый клиент и т.д.), обеспечивая при этом типобезопасность.

// Интерфейс с async-методами
interface IApiClient {
  get<T>(url: string): Promise<T>;
  post<T, R>(url: string, data: T): Promise<R>;
  put<T, R>(url: string, data: T): Promise<R>;
  delete(url: string): Promise<void>;
}

// Интерфейс для репозитория
interface IUserRepository {
  findById(id: number): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  create(data: CreateUserDTO): Promise<User>;
  update(id: number, data: UpdateUserDTO): Promise<User>;
  delete(id: number): Promise<boolean>;
}

// Реализация интерфейса
class ApiHttpClient implements IApiClient {
  async get<T>(url: string): Promise<T> {
    const response = await fetch(url);
    return response.json();
  }

  async post<T, R>(url: string, data: T): Promise<R> {
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    return response.json();
  }

  async put<T, R>(url: string, data: T): Promise<R> {
    const response = await fetch(url, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    return response.json();
  }

  async delete(url: string): Promise<void> {
    await fetch(url, { method: 'DELETE' });
  }
}
Практический совет: Используйте интерфейсы для определения контрактов между компонентами системы. Это упрощает тестирование (можно создать мок-реализации) и делает код более модульным. TypeScript проверит, что все методы интерфейса реализованы.

10. Последовательный await vs Параллельный await

Одна из важнейших концепций при работе с async/await — это понимание разницы между последовательным и параллельным выполнением асинхронных операций. При последовательном выполнении каждая операция начинается только после завершения предыдущей. При параллельном выполнении несколько операций запускаются одновременно, что может значительно улучшить производительность.

Последовательный await создаёт "водопад" из асинхронных вызовов, где каждый следующий вызов зависит от результата предыдущего. Это часто является антипаттерном, когда операции независимы друг от друга. В таких случаях лучше использовать параллельное выполнение с Promise.all.

Однако последовательное выполнение необходимо, когда одна операция зависит от результата другой. Например, сначала нужно получить ID пользователя, а затем получить его профиль. В таких случаях последовательный await является правильным подходом.

// ❌ Последовательное выполнение (медленно)
async function fetchDataSequential() {
  const users = await fetchUsers();     // Ждём 2 сек
  const posts = await fetchPosts();     // Ждём ещё 2 сек
  const comments = await fetchComments(); // Ждём ещё 2 сек
  // Итого: 6 секунд
}

// ✅ Параллельное выполнение (быстро)
async function fetchDataParallel() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchComments()
  ]);
  // Итого: 2 секунды (самая длинная операция)
}

// Смешанный подход: параллельно + последовательно
async function fetchUserData(userId: number) {
  // Параллельно: пользователь и настройки
  const [user, settings] = await Promise.all([
    fetchUser(userId),
    fetchSettings(userId)
  ]);

  // Последовательно: посты пользователя
  const posts = await fetchPosts(user.id);

  return { user, settings, posts };
}
Антипаттерн: Не используйте последовательный await для независимых операций. Это создаёт искусственные задержки и ухудшает пользовательский опыт. Если операции не зависят друг от друга, запускайте их параллельно с Promise.all.
💡 Совет учителя: Проведите эксперимент со студентами: создайте три асинхронные функции с задержкой и покажите разницу во времени выполнения между последовательным и параллельным подходами. Это наглядно продемонстрирует важность правильного использования await.

11. Promise.all с типами (кортежи)

Promise.all — это один из наиболее используемых методов для параллельного выполнения нескольких Promise. Он принимает массив Promise и возвращает один Promise, который разрешается массивом результатов. TypeScript обеспечивает типизацию для Promise.all, выводя типы элементов результирующего массива.

Одной из мощных особенностей TypeScript является возможность использования кортежей (tuple types) с Promise.all. Вместо обычного массива Promise можно передать кортеж, что позволяет TypeScript вывести более точные типы для каждого элемента результата. Это исключает необходимость в ручном приведении типов.

Кортежи с Promise.all особенно полезны, когда вы работаете с несколькими асинхронными операциями, каждая из которых возвращает разный тип. Это позволяет безопасно деструктурировать результат и использовать каждый элемент с правильным типом.

// Promise.all с типами
async function fetchDashboardData() {
  // Кортеж: TypeScript выводит точные типы
  const [users, posts, stats] = await Promise.all([
    fetchUsers(),    // Promise<User[]>
    fetchPosts(),    // Promise<Post[]>
    fetchStats()     // Promise<Stats>
  ]);
  // users: User[], posts: Post[], stats: Stats
}

// Promise.all с явной типизацией
type DashboardResult = [User[], Post[], Stats];

async function fetchDashboard(): Promise<DashboardResult> {
  const result: DashboardResult = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchStats()
  ]);
  return result;
}

// Деструктуризация с типами
const { users, posts, stats } = await (async () => {
  const [u, p, s] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchStats()
  ]);
  return { users: u, posts: p, stats: s };
})();
Преимущество кортежей: Использование кортежей с Promise.all позволяет TypeScript вывести точные типы для каждого элемента результата. Это исключает необходимость в ручном приведении типов и делает код более безопасным и читаемым.

12. Promise.race и Promise.allSettled с типами

Promise.race возвращает Promise, который разрешается первым из переданных Promise. TypeScript типизирует результат как union всех возможных типов, поскольку невозможно заранее знать, какой Promise завершится первым. Это важно учитывать при обработке результата.

Promise.allSettled возвращает Promise, который разрешается массивом объектов, описывающих результат каждого Promise. Каждый объект содержит либо { status: 'fulfilled', value: T }, либо { status: 'rejected', reason: any }. TypeScript корректно типизирует эти объекты, что позволяет безопасно обрабатывать как успешные, так и неудачные результаты.

Promise.allSettled особенно полезен, когда вы хотите выполнить несколько операций и обработать все результаты, даже если некоторые из них завершились ошибкой. Это отличный выбор для сценариев, где частичные результаты всё равно ценны.

// Promise.race с типами
async function fetchWithFallback<T>(
  primary: Promise<T>,
  fallback: Promise<T>
): Promise<T> {
  return Promise.race([primary, fallback]);
}

// Promise.allSettled с типами
interface SettledResult<T> {
  status: 'fulfilled';
  value: T;
}

interface RejectedResult {
  status: 'rejected';
  reason: unknown;
}

async function fetchMultipleUsers(ids: number[]) {
  const results = await Promise.allSettled(
    ids.map(id => fetchUser(id))
  );

  const successfulUsers = results
    .filter((r): r is SettledResult<User> => r.status === 'fulfilled')
    .map(r => r.value);

  const failedIds = results
    .filter((r): r is RejectedResult => r.status === 'rejected');

  return { successfulUsers, failedIds };
}
Тип guard: Используйте type guard для фильтрации результатов Promise.allSettled. Это позволяет TypeScript безопасно извлекать типы значений из результата, разделяя успешные и неудачные операции.
💡 Совет учителя: Объясните разницу между Promise.all (все должны выполниться успешно) и Promise.allSettled (обрабатываем каждый результат отдельно). Покажите, как type guard помогает безопасно работать с результатами allSettled.

13. Обработка ошибок с типами (try/catch)

Обработка ошибок в асинхронном коде — критически важный аспект надёжных приложений. В async-функциях используется стандартная конструкция try/catch для перехвата ошибок. TypeScript позволяет типизировать ошибки, что помогает в обработке различных типов ошибок.

В блоке catch переменная ошибки по умолчанию имеет тип unknown. Это безопасный тип, который требует проверки типа перед использованием. В старых версиях TypeScript переменная ошибки имела тип any, что было менее безопасно.

Для безопасной обработки ошибок рекомендуется использовать type guard для проверки типа ошибки. Это позволяет безопасно извлекать информацию об ошибке, сохраняя типобезопасность. Существует несколько подходов к типизации ошибок, каждый из которых имеет свои преимущества.

// Базовая обработка ошибок
async function fetchUserData(id: number): Promise<User> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    // error: unknown (в TypeScript 4.4+)
    if (error instanceof Error) {
      console.error('Ошибка:', error.message);
    }
    throw error;
  }
}

// Типизированная ошибка
class ApiError extends Error {
  constructor(
    public statusCode: number,
    public errorCode: string,
    message: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

async function fetchWithTypedErrors<T>(
  url: string
): Promise<T> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new ApiError(
        response.status,
        'API_ERROR',
        'Ошибка API'
      );
    }
    return await response.json();
  } catch (error) {
    if (error instanceof ApiError) {
      console.error(`API ошибка ${error.statusCode}: ${error.message}`);
    }
    throw error;
  }
}
Важно: Никогда не игнорируйте ошибки в async-функциях. Необработанные ошибки могут привести к непредсказуемому поведению приложения. Всегда используйте try/catch для обработки асинхронных ошибок и логируйте их для диагностики.

14. Типизация ошибок: unknown vs any

В TypeScript 4.4+ переменная ошибки в блоке catch имеет тип unknown по умолчанию. Это важное изменение безопасности типов, поскольку unknown требует явной проверки типа перед использованием. В отличие от any, unknown не позволяет выполнять какие-либо операции с переменной без предварительной проверки типа.

Использование unknown вместо any для типизации ошибок значительно повышает безопасность кода. Ошибки могут иметь любой тип — Error, строку, объект или даже undefined. Проверка типа гарантирует, что вы безопасно обрабатываете различные сценарии ошибок.

Существует несколько подходов к безопасной обработке ошибок с типом unknown: использование instanceof для проверки конкретных классов ошибок, использование type guard функций и создание утилит для безопасного извлечения сообщений об ошибках.

// Безопасная обработка ошибок с unknown
function isError(value: unknown): value is Error {
  return value instanceof Error;
}

function getErrorMessage(error: unknown): string {
  if (error instanceof Error) {
    return error.message;
  }
  if (typeof error === 'string') {
    return error;
  }
  if (typeof error === 'object' && error !== null && 'message' in error) {
    return String((error as { message: unknown }).message);
  }
  return 'Неизвестная ошибка';
}

// Использование
async function safeFetch(url: string): Promise<Response | null> {
  try {
    return await fetch(url);
  } catch (error) {
    console.error(getErrorMessage(error));
    return null;
  }
}

// ❌ Небезопасно: any позволяет всё
function unsafeHandler(error: any) {
  console.log(error.message); // Без проверки!
  console.log(error.code); // Может не существовать!
}

// ✅ Безопасно: unknown требует проверки
function safeHandler(error: unknown) {
  if (error instanceof Error) {
    console.log(error.message); // Безопасно
  }
}
Правило: Всегда используйте unknown для типизации ошибок в блоке catch. Это вынуждает вас проверять тип ошибки перед использованием, что предотвращает runtime-ошибки и делает код более надёжным.

15. Async-итераторы (AsyncIterable<T>)

Async-итераторы позволяют перебирать асинхронные последовательности данных. В отличие от синхронных итераторов, async-итераторы возвращают Promise для каждого значения, что позволяет работать с асинхронными источниками данных, такими как базы данных, файловые системы или сетевые запросы.

Для определения async-итератора используется интерфейс AsyncIterator, который содержит метод next(), возвращающий Promise<IteratorResult<T>>. Интерфейс AsyncIterable определяет метод [Symbol.asyncIterator](), который возвращает AsyncIterator.

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

// Async-итератор
class AsyncNumberGenerator implements AsyncIterable<number> {
  private current = 0;
  private limit: number;

  constructor(limit: number) {
    this.limit = limit;
  }

  [Symbol.asyncIterator](): AsyncIterator<number> {
    return {
      next: async (): Promise<IteratorResult<number>> => {
        if (this.current < this.limit) {
          return { value: this.current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

// Использование async-for-of
async function processNumbers() {
  const generator = new AsyncNumberGenerator(10);

  for await (const num of generator) {
    console.log(num);
  }
}

// Async-итератор для чтения файла
async function* readLines(filePath: string): AsyncGenerator<string> {
  const content = await fs.readFile(filePath, 'utf-8');
  const lines = content.split('\n');
  for (const line of lines) {
    yield line;
  }
}
Применение: Async-итераторы идеально подходят для обработки стриминговых данных, чтения больших файлов построчно, обработки страниц результатов API и обработки данных из базы данных порциями.

16. Async-генераторы

Async-генераторы — это гибридная конструкция, объединяющая возможности генераторов (yield) и async-функций (await). Они возвращают AsyncGenerator, который реализует интерфейс AsyncIterable, что позволяет использовать конструкцию for await...of для перебора значений.

Async-генераторы особенно полезны для создания асинхронных последовательностей, где каждое значение может вычисляться асинхронно. Они позволяют "лениво" вычислять значения по мере необходимости, что оптимизирует использование ресурсов.

Синтаксис async-генераторов аналогичен синхронным генераторам, но с добавлением ключевого слова async перед function. Ключевое слово yield используется для возврата значения, а await — для ожидания асинхронных операций внутри генератора.

// Async-генератор
async function* fetchPages(
  baseUrl: string,
  totalPages: number
): AsyncGenerator<Page> {
  for (let page = 1; page <= totalPages; page++) {
    const response = await fetch(`${baseUrl}?page=${page}`);
    const data = await response.json();
    yield data;
  }
}

// Async-генератор с обработкой ошибок
async function* retryFetch<T>(
  url: string,
  maxRetries: number = 3
): AsyncGenerator<T, void, unknown> {
  let attempts = 0;

  while (attempts < maxRetries) {
    try {
      const response = await fetch(url);
      const data = await response.json();
      yield data;
      return;
    } catch (error) {
      attempts++;
      console.log(`Попытка ${attempts} из ${maxRetries}`);
      if (attempts === maxRetries) throw error;
    }
  }
}

// Использование
for await (const page of fetchPages('/api/pages', 5)) {
  console.log('Страница:', page);
}
💡 Совет учителя: Покажите, как async-генераторы позволяют создавать ленивые асинхронные последовательности. Это полезно для обработки больших объёмов данных, где не нужно загружать всё в память одновременно. Свяжите это с реальными сценариями использования, например, пагинация API.

17. Typed retry паттерн

Паттерн retry (повторная попытка) широко используется при работе с сетевыми запросами и внешними API. Он позволяет повторять операцию при временных сбоях, значительно повышая надёжность приложения. TypeScript позволяет создать типизированный retry-хелпер, который сохраняет информацию о типах на каждом этапе.

Типизированный retry-хелпер должен поддерживать generic-параметры для определения типа возвращаемого значения и типа ошибки. Это позволяет использовать его с различными асинхронными функциями, сохраняя типобезопасность.

При реализации retry-паттерна важно учитывать стратегии задержки (exponential backoff), максимальное количество попыток и условия, при которых повторная попытка не имеет смысла (например, ошибка 404 — не требует повтора).

// Typed retry паттерн
interface RetryOptions {
  maxRetries: number;
  delay: number;
  backoff?: 'linear' | 'exponential';
  retryOn?: (error: unknown) => boolean;
}

async function retry<T>(
  fn: () => Promise<T>,
  options: RetryOptions
): Promise<T> {
  const { maxRetries, delay, backoff = 'exponential', retryOn } = options;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries) throw error;

      if (retryOn && !retryOn(error)) {
        throw error;
      }

      const waitTime = backoff === 'exponential'
      ? delay * Math.pow(2, attempt)
      : delay * (attempt + 1);

      await new Promise(r => setTimeout(r, waitTime));
    }
  }
  throw new Error('Должно быть недостижимо');
}

// Использование
const data = await retry(
  () => fetchData<User>('/api/user/1'),
  {
    maxRetries: 3,
    delay: 1000,
    backoff: 'exponential',
    retryOn: (error) => {
      if (error instanceof ApiError) {
        return error.statusCode >= 500;
      }
      return false;
    }
  }
);
Преимущество типизации: Благодаря generic-параметрам функция retry автоматически выводит тип возвращаемого значения. Вы получаете типизированный результат без необходимости в ручном приведении типов.

18. Typed timeout паттерн

Паттерн timeout используется для ограничения времени ожидания асинхронной операции. Если операция не завершается за заданное время, она отменяется или выбрасывается ошибка. TypeScript позволяет создать типизированный timeout-хелпер, который корректно обрабатывает типы возвращаемых значений и ошибок.

При реализации timeout-паттерна важно корректно типизировать как успешный результат, так и ошибку таймаута. Это позволяет вызывающему коду безопасно обрабатывать оба сценария.

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

// Typed timeout паттерн
class TimeoutError extends Error {
  constructor(public timeout: number) {
    super(`Операция превысила лимит ${timeout}мс`);
    this.name = 'TimeoutError';
  }
}

async function withTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number
): Promise<T> {
  let timeoutId: ReturnType<typeof setTimeout>;

  const timeoutPromise = new Promise<never>((_, reject) => {
    timeoutId = setTimeout(() => {
      reject(new TimeoutError(timeoutMs));
    }, timeoutMs);
  });

  try {
    return await Promise.race([promise, timeoutPromise]);
  } finally {
    clearTimeout(timeoutId);
  }
}

// Использование
try {
  const data = await withTimeout(
    fetchLargeDataset(),
    5000
  );
  console.log('Данные получены:', data);
} catch (error) {
  if (error instanceof TimeoutError) {
    console.error(`Превышен лимит ${error.timeout}мс`);
  }
}
Важно: При реализации timeout-паттерна не забывайте очищать таймер в блоке finally, чтобы избежать утечки памяти. Также убедитесь, что оригинальный Promise не остаётся висящим после таймаута.

19. Typed throttle/debounce

Throttle и debounce — это паттерны ограничения частоты вызова функций. Throttle ограничивает количество вызовов в единицу времени, а debounce откладывает вызов до тех пор, пока не пройдёт определённое время с последнего вызова. TypeScript позволяет создать типизированные версии этих утилит.

Типизация throttle и debounce особенно важна, поскольку эти функции часто используются для обработки событий ввода, поиска и других интерактивных элементов. Корректная типизация гарантирует, что обработчики событий будут иметь правильные типы параметров.

При работе с асинхронными функциями throttle и debounce должны корректно обрабатывать Promise, что требует специальных подходов к типизации.

// Typed debounce
function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): T {
  let timeoutId: ReturnType<typeof setTimeout>;

  return ((...args: Parameters<T>) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  }) as T;
}

// Typed throttle
function throttle<T extends (...args: any[]) => any>(
  fn: T,
  limit: number
): T {
  let inThrottle = false;

  return ((...args: Parameters<T>) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  }) as T;
}

// Async debounce
function debounceAsync<T extends (...args: any[]) => Promise<any>>(
  fn: T,
  delay: number
): T {
  let timeoutId: ReturnType<typeof setTimeout>;

  return ((...args: Parameters<T>) => {
    return new Promise((resolve, reject) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(async () => {
        try {
          const result = await fn(...args);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      }, delay);
    });
  }) as T;
}

// Использование
const search = debounce(async (query: string) => {
  const results = await searchApi(query);
  return results;
}, 300);
Применение: Throttle и debounce часто используются для обработки событий прокрутки, ввода текста в поле поиска, изменения размера окна и других частых событий. Типизация гарантирует, что обработчики будут иметь правильные типы.

20. Async middleware typing

Middleware — это функции, которые обрабатывают запросы до того, как они достигнут основного обработчика. В серверных фреймворках (Express, Koa, Fastify) middleware играют ключевую роль в обработке запросов. TypeScript позволяет создать строго типизированные middleware, что обеспечивает безопасность типов на каждом этапе обработки.

Типизация middleware особенно важна для поддержания контекста запроса (request context), который передаётся через цепочку middleware. TypeScript гарантирует, что каждый middleware может добавлять данные в контекст, и эти данные будут доступны последующим middleware и обработчикам.

Для типизации middleware используются generic-параметры и дополнение типов (type augmentation). Это позволяет создавать расширяемую систему, где новые middleware могут добавлять свойства в контекст без потери типобезопасности.

// Типизация middleware
interface Request {
  path: string;
  method: string;
  headers: Record<string, string>;
}

interface Response {
  status(code: number): Response;
  json(data: any): void;
}

type Middleware<TContext = {}> = (
  req: Request,
  res: Response,
  next: () => Promise<void>,
  context: TContext
) => Promise<void>;

// Auth middleware
interface AuthContext {
  user?: User;
  isAuthenticated: boolean;
}

const authMiddleware: Middleware<AuthContext> = async (
  req, res, next, context
) => {
  const token = req.headers.authorization;

  if (token) {
    context.user = await verifyToken(token);
    context.isAuthenticated = true;
  } else {
    context.isAuthenticated = false;
  }

  await next();
};

// Rate limit middleware
interface RateLimitContext {
  rateLimit: { remaining: number; reset: number };
}

const rateLimitMiddleware: Middleware<RateLimitContext> = async (
  req, res, next, context
) => {
  context.rateLimit = await checkRateLimit(req);
  if (context.rateLimit.remaining <= 0) {
    res.status(429).json({ error: 'Rate limit exceeded' });
    return;
  }
  await next();
};
💡 Совет учителя: Покажите, как generic-параметры позволяют создавать типизированные middleware, которые могут расширять контекст запроса. Это основа для создания расширяемых и типобезопасных серверных приложений.

21. Async/await с generic-функциями

Generic-функции в TypeScript позволяют создавать универсальные функции, которые работают с различными типами. В сочетании с async/await они создают мощные утилиты, которые могут обрабатывать данные любого типа, сохраняя при этом полную типобезопасность.

Типизация generic-async-функций требует особого внимания к возвращаемому типу. TypeScript должен правильно выводить generic-параметры на основе входных данных и возвращаемого значения. Это достигается за счёт использования ограничений (constraints) и вывода типов.

Generic-async-функции широко используются для создания API-клиентов, обработчиков данных и утилит для работы с Promise. Они позволяют избежать дублирования кода и создавать компоненты, которые работают с любыми типами данных.

// Generic async-функции
async function fetchJson<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

// Generic async с ограничением типа
async function processData<T extends { id: number }>(
  items: T[],
  processor: (item: T) => Promise<T>
): Promise<T[]> {
  return Promise.all(items.map(processor));
}

// Generic async с несколькими параметрами
async function transformData<TInput, TOutput>(
  data: TInput,
  transformer: (input: TInput) => Promise<TOutput>
): Promise<TOutput> {
  return transformer(data);
}

// Использование
interface User { id: number; name: string }
interface UserDto { id: number; displayName: string }

const user = await fetchJson<User>('/api/user/1');
// user: User

const dto = await transformData<User, UserDto>(
  user,
  async (u) => ({ id: u.id, displayName: u.name })
);
// dto: UserDto
Совет: Используйте ограничения generics (extends) для обеспечения того, что входные данные имеют необходимые свойства. Это помогает TypeScript выводить более точные типы и ловить ошибки на этапе компиляции.

22. Типизация async callback-хелперов

Callback-хелперы — это функции, которые упрощают использование callback-стиля программирования. При работе с асинхронными операциями callback-хелперы должны корректно обрабатывать как результаты, так и ошибки. TypeScript позволяет создать типизированные callback-хелперы, которые обеспечивают безопасность типов.

Типизация callback-хелперов особенно важна при работе с промисификации — преобразовании callback-стиля кода в Promise. Это позволяет использовать async/await с функциями, которые изначально используют callback-стиль.

Типизированные callback-хелперы также полезны для создания обёрток над функциями с несколькими callback-аргументами (error-first, success-first и т.д.). TypeScript гарантирует, что типы аргументов callback-функций будут корректно выведены.

// Промисификация с типизацией
function promisify<T>(
  fn: (callback: (error: Error | null, result: T) => void) => void
): () => Promise<T> {
  return () => new Promise((resolve, reject) => {
    fn((error, result) => {
      if (error) reject(error);
      else resolve(result);
    });
  });
}

// Промисификация с аргументами
function promisifyWithArgs<T, TArgs extends any[]>(
  fn: (...args: [...TArgs, (error: Error | null, result: T) => void]) => void
): (...args: TArgs) => Promise<T> {
  return (...args: TArgs) =>
    new Promise((resolve, reject) => {
      fn(...args, (error, result) => {
        if (error) reject(error);
        else resolve(result);
      });
    });
}

// Использование
const readFileAsync = promisifyWithArgs(fs.readFile);
// readFileAsync: (path: string, encoding: string) => Promise<string>

const content = await readFileAsync('/path/to/file', 'utf-8');
Применение: Промисификация позволяет использовать async/await с устаревшими API, которые используют callback-стиль. Это особенно полезно при миграции Legacy-кода на современный JavaScript.

23. Реальные примеры: API клиент

Создание типизированного API-клиента — одна из наиболее частых задач при работе с асинхронным кодом. TypeScript позволяет создать клиент, который обеспечивает полную типобезопасность при работе с REST или GraphQL API. Это включает типизацию запросов, ответов, ошибок и middleware.

Типизированный API-клиент должен поддерживать generic-параметры для определения типов возвращаемых данных. Это позволяет использовать один клиент для различных эндпоинтов, сохраняя информацию о типах на каждом этапе.

При создании API-клиента важно учитывать обработку ошибок, retry-логику, таймауты и middleware. TypeScript позволяет типизировать все эти аспекты, создавая надёжную и расширяемую систему.

// Типизированный API-клиент
interface ApiConfig {
  baseUrl: string;
  headers?: Record<string, string>;
  timeout?: number;
}

interface RequestOptions<TBody = unknown> {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  body?: TBody;
  headers?: Record<string, string>;
}

class ApiClient {
  private config: ApiConfig;

  constructor(config: ApiConfig) {
    this.config = config;
  }

  async request<TResponse, TBody = unknown>(
    endpoint: string,
    options: RequestOptions<TBody> = {}
  ): Promise<TResponse> {
    const url = `${this.config.baseUrl}${endpoint}`;
    const headers = {
      ...this.config.headers,
      ...options.headers,
      (options.body && { 'Content-Type': 'application/json' })
    };

    const response = await fetch(url, {
      method: options.method || 'GET',
      headers,
      body: options.body
      ? JSON.stringify(options.body)
      : undefined
    });

    if (!response.ok) {
      throw new ApiError(response.status, response.statusText);
    }

    return response.json();
  }

  get<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint);
  }

  post<TResponse, TBody>(
    endpoint: string,
    body: TBody
  ): Promise<TResponse> {
    return this.request<TResponse, TBody>(endpoint, { method: 'POST', body });
  }
}

// Использование
const api = new ApiClient({ baseUrl: 'https://api.example.com' });

const users = await api.get<User[]>('/users');
const newUser = await api.post<User, CreateUserDTO>(
  '/users',
  { name: 'John', email: 'john@example.com' }
);
💡 Совет учителя: Покажите, как generic-параметры позволяют создавать универсальный API-клиент, который работает с любыми типами данных. Это демонстрирует мощь TypeScript при создании переиспользуемых компонентов.

24. Реальные примеры: Загрузка данных

Загрузка данных — одна из наиболее распространённых задач в веб-приложениях. TypeScript позволяет создать типизированные хуки и утилиты для загрузки данных, которые обеспечивают безопасность типов на каждом этапе: от запроса до отображения данных в пользовательском интерфейсе.

При создании системы загрузки данных важно учитывать различные состояния: загрузка, успешный результат, ошибка и начальное состояние. TypeScript позволяет типизировать каждое из этих состояний, что помогает избежать ошибок при обработке данных.

Также важно учитывать обработку ошибок, повторные попытки и кэширование. TypeScript позволяет создать типизированные системы, которые обрабатывают все эти аспекты, обеспечивая надёжность и производительность приложения.

// Типизированная загрузка данных
interface AsyncState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

class DataLoader<T> {
  private state: AsyncState<T> = {
    data: null,
    loading: false,
    error: null
  };

  private cache = new Map<string, T>();

  async load(
    key: string,
    fetcher: () => Promise<T>
  ): Promise<T> {
    // Проверяем кэш
    if (this.cache.has(key)) {
      return this.cache.get(key)!;
    }

    this.state = { ...this.state, loading: true, error: null };

    try {
      const data = await fetcher();
      this.cache.set(key, data);
      this.state = { data, loading: false, error: null };
      return data;
    } catch (error) {
      this.state = {
        ...this.state,
        loading: false,
        error: error instanceof Error ? error : new Error('Unknown error')
      };
      throw error;
    }
  }

  getState(): Readonly<AsyncState<T>> {
    return this.state;
  }
}

// Использование
const userLoader = new DataLoader<User>();

const user = await userLoader.load('user:1', () => fetchUser(1));
const state = userLoader.getState();
Ключевые преимущества: Типизированная загрузка данных обеспечивает безопасность типов на каждом этапе: от запроса до отображения. Это помогает ловить ошибки на этапе компиляции и делает код более предсказуемым.

25. Типичные ошибки при работе с async/await

При работе с async/await и типами TypeScript разработчики часто допускают ошибки, которые могут привести к проблемам с типами или runtime-ошибкам. Понимание этих типичных ошибок поможет избежать их в собственном коде.

Одна из наиболее распространённых ошибок — это использование await вне async-функции. TypeScript не позволяет этого, но ошибка может возникнуть при работе с Promise без await. Другая распространённая ошибка — это неправильная обработка ошибок, когда ошибки игнорируются или обрабатываются неправильно.

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

// ❌ Ошибка: await вне async-функции
const data = await fetchData(); // Ошибка компиляции!

// ✅ Правильно:
async function main() {
  const data = await fetchData();
}

// ❌ Ошибка: игнорирование ошибок
async function badFetch() {
  const data = await fetchData(); // Нет обработки ошибок!
  return data;
}

// ✅ Правильно:
async function goodFetch() {
  try {
    const data = await fetchData();
    return data;
  } catch (error) {
    console.error('Ошибка:', error);
    throw error;
  }
}

// ❌ Ошибка: последовательный await для независимых операций
async function badParallel() {
  const a = await fetchA();
  const b = await fetchB();
  const c = await fetchC();
}

// ✅ Правильно:
async function goodParallel() {
  const [a, b, c] = await Promise.all([
    fetchA(),
    fetchB(),
    fetchC()
  ]);
}

// ❌ Ошибка: неправильная обработка ошибок в Promise.all
async function badPromiseAll() {
  try {
    const [a, b] = await Promise.all([
      fetchA(),
      fetchB()
    ]);
  } catch (error) {
    // Ошибка в одном промисе отменяет оба!
  }
}

// ✅ Правильно: Promise.allSettled
async function goodPromiseAll() {
  const results = await Promise.allSettled([
    fetchA(),
    fetchB()
  ]);
  // Каждый результат обрабатывается отдельно
}
Важно: Избегайте антипаттернов при работе с async/await. Всегда обрабатывайте ошибки, используйте параллельное выполнение для независимых операций и правильно типизируйте Promise.
💡 Совет учителя: Покажите студентам каждую ошибку и её исправление. Используйте примеры из реальных проектов, чтобы студенты могли связать теорию с практикой. Обсудите, почему каждый антипаттерн является проблемой и как его избежать.

26. Лучшие практики и советы

В этом разделе мы собрали лучшие практики и советы по работе с async/await и типами TypeScript. Следование этим рекомендациям поможет вам писать более безопасный, читаемый и поддерживаемый код.

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

Используйте Promise.all для параллельного выполнения независимых операций. Последовательный await для независимых операций — антипаттерн, который создаёт искусственные задержки. Используйте Promise.allSettled, когда частичные результаты всё равно ценны.

Всегда обрабатывайте ошибки в async-функциях. Используйте try/catch и проверяйте тип ошибки с помощью instanceof или type guard. Никогда не игнорируйте ошибки — это может привести к трудноуловимым runtime-ошибкам.

Используйте unknown вместо any для типизации ошибок. Unknown требует проверки типа перед использованием, что делает код более безопасным. Создавайте утилиты для безопасного извлечения сообщений об ошибках.

Применяйте generic-параметры для создания переиспользуемых асинхронных функций. Это позволяет создавать универсальные утилиты, которые работают с любыми типами данных, сохраняя при этом полную типобезопасность.

Создавайте типизированные обёртки для работы с retry, timeout и debounce. Это не только повышает безопасность типов, но и делает код более декларативным и лёгким для чтения.

// Сводка лучших практик

// 1. Явная аннотация возвращаемого типа
async function getUser(id: number): Promise<User | null> {
  // ...
}

// 2. Обработка ошибок с unknown
async function safeFetch() {
  try {
    return await fetchData();
  } catch (error: unknown) {
    if (error instanceof Error) {
      console.error(error.message);
    }
    throw error;
  }
}

// 3. Параллельное выполнение
async function loadDashboard() {
  const [users, posts] = await Promise.all([
    fetchUsers(),
    fetchPosts()
  ]);
}

// 4. Generic-функции для переиспользования
async function fetchJson<T>(url: string): Promise<T> {
  // ...
}

// 5. Типизированные утилиты
const search = debounce<
  (query: string) => Promise<SearchResult[]>
>(searchApi, 300);
Итог: Соблюдение лучших практик при работе с async/await и типами TypeScript значительно повышает качество кода, уменьшает количество ошибок и упрощает поддержку и развитие проекта.
💡 Совет учителя: Проведите код-ревью с студентами, используя приведённые выше примеры. Обсудите, почему каждая практика является лучшей и как она влияет на качество кода. Дайте студентам задание на рефакторинг существующего кода с использованием этих практик.

Завершение урока

В этом уроке мы подробно изучили async/await с типами TypeScript. Вы узнали о типизации Promise, обработке ошибок, использовании generics и создании типизированных утилит. Эти знания позволят вам писать безопасный и поддерживаемый асинхронный код.

Практикуйтесь, экспериментируйте и применяйте изученное в реальных проектах!

Тест: async/await с типами

10 вопросов

async/await с типами

Premium