$ sudo teach IT

Модуль 10: Типизация Promise

Полное руководство по работе с типами промисов в TypeScript

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

1. Что такое Promise и зачем его типизировать?

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

Типизация Promise важна по нескольким причинам:

  • Безопасность типов — вы точно знаете, какой тип данных получите
  • Автодополнение — IDE подсказывает доступные методы и свойства
  • Выявление ошибок — неправильное использование типа будет выявлено на этапе компиляции
  • Документация — тип служит живой документацией кода
Важно: Без типизации промисов вы работаете вслепую — не знаете, какой тип вернётся из асинхронной функции, что приводит квыполнение-time ошибкам.

2. Базовый синтаксис Promise<T>

Generic-тип Promise принимает один параметр — тип значения, которое будет передано в resolve:

const promise: Promise<string> = new Promise((resolve, reject) => {
  resolve('Привет, мир!');
});

const result: string = await promise;
// result имеет тип string

Здесь Promise<string> означает, что промис будет резолвиться со значением типа string.

Совет: Всегда указывайте тип generic явно, когда создаёте промис вручную. Это помогает избежать неожиданных ошибок.

3. Promise<void>

Promise<void> используется, когда промис не возвращает полезного значения. Это типичная ситуация для функций, которые выполняютпобочный эффект (side effects):

async function logMessage(message: string): Promise<void> {
  console.log(message);
}

async function saveToDatabase(data: UserData): Promise<void> {
  await database.insert(data);
}

async function sendNotification(email: string): Promise<void> {
  await emailService.send(email);
}
Обратите внимание: Promise<void> не означает, что промис ничего не делает — он выполняет работу, но не возвращает значение. Это принципиальное отличие от Promise<undefined>.
Разница между void и undefined:
  • Promise<void> — промис ничего не возвращает (намеренно)
  • Promise<undefined> — промис возвращает undefined

4. Promise<never>

Promise<never> используется, когда промис никогда не завершается успешно — он всегда отклоняется. Это полезно для типизации функций, которые выбрасывают ошибки:

function throwError(message: string): Promise<never> {
  return Promise.reject(new Error(message));
}

async function alwaysFails(): Promise<never> {
  throw new Error('Всегда падает!');
}
Предупреждение: Тип never является подтипом всех типов. Это значит, что Promise<never> совместим с любым другим типом промиса, что может привести к неожиданным последствиям.

5. Promise<Promise<T>> — распаковка промисов

TypeScript автоматически распаковывает вложенные промисы. Если функция возвращает Promise<Promise<string>>, TypeScript автоматически приведёт его к Promise<string>:

async function fetchUser(id: number): Promise<User> {
  return fetch(`/api/users/${id}`);
}

async function fetchUserPosts(userId: number): Promise<Post[]> {
  const user = await fetchUser(userId); // User, не Promise<User>
  return fetchPosts(user.id);
}

// Автоматическая распаковка
const result1 = await fetchUser(1);
// result1: User (не Promise<User>)

const result2 = fetchUser(1);
// result2: Promise<User>
Правило распаковки: TypeScript автоматически распаковывает один уровень вложенности промисов. Promise<Promise<T>> становится Promise<T>, но Promise<Promise<Promise<T>>> станет Promise<Promise<T>>.

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

Когда вы работаете с массивом промисов, важно правильно типизировать результат:

// Массив промисов
const promises: Promise<string>[] = [
  fetchData('url1'),
  fetchData('url2'),
  fetchData('url3')
];

// Ожидание всех промисов
const results: string[] = await Promise.all(promises);
// results: string[]

// С результатами и ошибками
const results2: PromiseSettledResult<string>[] = await Promise.allSettled(promises);
Совет: Используйте Promise.allSettled() вместо Promise.all(), когда вам важно получить результаты всех промисов, даже если некоторые из них отклонились.

7. Кортежи промисов (Promise Tuples)

Кортежи промисов позволяют типизировать результаты разных асинхронных операций с разными типами:

// Кортеж из промисов разных типов
const tuple: [Promise<string>, Promise<number>, Promise<boolean>] = [
  fetchUserName(),
  fetchUserAge(),
  fetchIsActive()
];

const [name, age, isActive] = await Promise.all(tuple);
// name: string, age: number, isActive: boolean
Преимущество кортежей: TypeScript точно знает тип каждого элемента в результате, что позволяет безопасно деструктуризировать промисы.

8. Типизация resolve и reject

При создании промиса вручную важно правильно типизировать параметры resolve и reject:

// Правильная типизация
const promise1 = new Promise<number>((resolve, reject) => {
  resolve(42); // ✓ number
  resolve('строка'); // ✗ Ошибка компиляции!
});

const promise2 = new Promise<string, Error>((resolve, reject) => {
  resolve('успех'); // ✓ string
  reject(new Error('ошибка')); // ✓ Error
});
Важно: Второй generic-параметр Promise (тип reject) по умолчанию any. Явно указывайте его для строгой типизации ошибок.

9. Промисификация с типами

Промисификация — это процесс преобразования callback-based функции в функцию, возвращающую Promise. TypeScript позволяет безопасно типизировать этот процесс:

// Callback-based функция
function readFileCallback(
  path: string,
  callback: (error: Error | null, data: string) => void
): void { ... }

// Промисифицированная версия
function readFile(path: string): Promise<string> {
  return new Promise((resolve, reject) => {
    readFileCallback(path, (error, data) => {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

// Использование
const content: string = await readFile('/path/to/file.txt');
Совет: Всегда указывайте возвращаемый тип промиса явно при промисификации — это гарантирует корректность типов для всех потребителей функции.

10. Generic-функции, возвращающие Promise<T>

Generic-функции позволяют создавать переиспользуемый асинхронный код с правильной типизацией:

// Generic async функция
async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json();
}

// Использование с конкретным типом
interface User {
  id: number;
  name: string;
  email: string;
}

const user: User = await fetchData<User>('/api/user/1');
// user имеет тип User
Паттерн: Generic-функции с Promise — мощный инструмент для создания типобезопасных обёрток над API-вызовами.

11. Частые ошибки типизации промисов

Рассмотрим наиболее распространённые ошибки при работе с типами промисов:

// ❌ Ошибка 1: Забыли await
function getUser(): Promise<User> {
  return fetchUser(1);
}

const user = getUser();
// user: Promise<User>, а не User!

// ❌ Ошибка 2: Неправильная типизация reject
const promise = new Promise<string>((resolve, reject) => {
  reject('ошибка'); // OK, но нет контроля типа
});

// ✅ Правильно: указываем тип reject
const promise2 = new Promise<string, Error>((resolve, reject) => {
  reject(new Error('ошибка'));
});
Частая ошибка: Использование Promise<any> или отсутствие типизации — это антипаттерн. Всегда указывайте конкретные типы.

12. Promise с union типами

Иногда промис может резолвиться с разными типами значений. В таких случаях используйте union типы:

// Union тип для промиса
type ApiResult = User | Post | Comment;

async function fetchItem(type: string, id: number): Promise<ApiResult> {
  return fetch(`/api/${type}/${id}`).then(r => r.json());
}

// Сужение типа после получения
const result = await fetchItem('users', 1);
if ('name' in result) {
  // result: User
  console.log(result.name);
}

13. Promise с условными типами

Условные типы позволяют создавать более гибкие типы промисов:

// Условный тип для промиса
type Awaited<T> = T extends Promise<infer U> ? U : T;

type Result1 = Awaited<Promise<string>>; // string
type Result2 = Awaited<Promise<number>>; // number
type Result3 = Awaited<string>; // string (не промис)

// Функция с условным типом
function processValue<T>(
  value: T extends Promise<infer U> ? U : T
): T extends Promise<infer U> ? Promise<U> : T {
  return value;
}

14. Promise с mapped типами

Mapped типы позволяют автоматически типизировать объекты с асинхронными свойствами:

// Преобразование типов свойств в Promise
type Async<T> = {
  [K in keyof T]: Promise<T[K]>;
};

// Пример использования
interface User {
  name: string;
  age: number;
}

type AsyncUser = Async<User>;
// { name: Promise<string>; age: Promise<number>; }

// Функция для загрузки всех свойств
async function loadUser(): Promise<User> {
  const asyncUser: AsyncUser = {
    name: fetchName(),
    age: fetchAge()
  };
  
  return {
    name: await asyncUser.name,
    age: await asyncUser.age
  };
}

15. Пример: Типизация API-клиента

Практический пример создания типобезопасного API-клиента:

interface ApiEndpoints {
  '/users': User[];
  '/users/:id': User;
  '/posts': Post[];
  '/posts/:id': Post;
}

class ApiClient {
  async get<K extends keyof ApiEndpoints>(
    endpoint: K
  ): Promise<ApiEndpoints[K]> {
    const response = await fetch(endpoint);
    return response.json();
  }
}

const api = new ApiClient();

// Автоматический вывод типов
const users: User[] = await api.get('/users');
const user: User = await api.get('/users/1');

16. Пример: Цепочки промисов

Правильная типизация цепочек промисов критически важна для поддержания типа на каждом шаге:

// Цепочка с правильной типизацией
function processData(input: string): Promise<ProcessedResult> {
  return fetchData(input)
    .then((data: RawData) => validate(data))
    .then((validated: ValidatedData) => transform(validated))
    .then((result: ProcessedResult) => result);
}

// TypeScript автоматически выводит тип каждого шага
const result: ProcessedResult = await processData('input');
Совет: Используйте async/await вместо цепочек .then() — это делает код более читаемым и упрощает типизацию.

17. Пример: Работа с Promise.all

Promise.all возвращает кортеж с типами каждого промиса:

// Параллельная загрузка данных
async function loadDashboard(): Promise<{
  user: User;
  posts: Post[];
  notifications: Notification[];
}> {
  const [user, posts, notifications] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchNotifications()
  ]);
  
  return { user, posts, notifications };
}

// TypeScript знает тип каждого элемента
const dashboard = await loadDashboard();
console.log(dashboard.user.name); // ✓ string

18. Пример: Обработка ошибок с типами

Правильная типизация обработки ошибок в промисах:

// Кастомные ошибки с типами
class ValidationError extends Error {
  constructor(public field: string) {
    super(`Validation failed for field: ${field}`);
  }
}

class NotFoundError extends Error {
  constructor(public id: number) {
    super(`User with id ${id} not found`);
  }
}

// Функция с типизированными ошибками
async function getUser(id: number): Promise<User> {
  if (id <= 0) {
    throw new ValidationError('id');
  }
  
  const user = await database.findUser(id);
  if (!user) {
    throw new NotFoundError(id);
  }
  
  return user;
}

// Обработка с проверкой типа
try {
  const user = await getUser(1);
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(error.field); // ✓ string
  } else if (error instanceof NotFoundError) {
    console.log(error.id); // ✓ number
  }
}

19. Пример: Retry механизм с типами

Создание типобезопасного механизма повторных попыток:

// Generic retry функция
async function retry<T>(
  fn: () => Promise<T>,
  maxAttempts: number = 3,
  delay: number = 1000
): Promise<T> {
  let lastError: Error;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      if (attempt < maxAttempts) {
        await new Promise(r => setTimeout(r, delay));
      }
    }
  }
  
  throw lastError!;
}

// Использование
const user: User = await retry(
  () => fetchUser(1),
  3,
  1000
);

20. Пример: Кэширование с типами

Создание типобезопасного кэша для асинхронных операций:

// Generic кэш для промисов
class PromiseCache<K, V> {
  private cache = new Map<K, Promise<V>>();
  
  async get(
    key: K,
    fetcher: () => Promise<V>
  ): Promise<V> {
    if (this.cache.has(key)) {
      return this.cache.get(key)!;
    }
    
    const promise = fetcher();
    this.cache.set(key, promise);
    
    return promise;
  }
}

// Использование
const userCache = new PromiseCache<number, User>();

const user1 = await userCache.get(1, () => fetchUser(1));
const user2 = await userCache.get(1, () => fetchUser(1));
// user1 === user2 (та же ссылка)

21. Промисы и типобезопасные утилиты

Создание утилитных типов для работы с промисами:

// Утилитные типы для промисов
type Promisify<T> = T extends (...args: any[]) => infer R
  ? (...args: any[]) => Promise<R>
  : never;

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type PromiseValue<T> = T extends Promise<infer U>
  ? PromiseValue<U>
  : T;

// Примеры использования
function syncFn(x: number): string { return x.toString(); }

type AsyncFn = Promisify<typeof syncFn>;
// (x: number) => Promise<string>

type Nested = Promise<Promise<string>>;
type Unwrapped = PromiseValue<Nested>; // string

22. Итоги и лучшие практики

Лучшие практики типизации Promise:
  • Всегда указывайте тип generic при создании промиса
  • Используйте Promise<void> для функций без возвращаемого значения
  • Типизируйте reject — не оставляйте any
  • Избегайте Promise<any> — это антипаттерн
  • Используйте generic-функции для переиспользования
  • Предпочитайте async/await цепочкам .then()
  • Создавайте типобезопасные обёртки над API
Помните: Правильная типизация промисов — это инвестиция в надёжность и поддерживаемость вашего кода.

Тест: Типизация Promise

10 вопросов

Типизированный Promise

Premium