Модуль 10: Типизация Promise
Полное руководство по работе с типами промисов в TypeScript
1. Что такое Promise и зачем его типизировать?
Promise — это объект, представляющий собой результат завершённой или ещё не завершённой асинхронной операции. В JavaScript промис может работать с любыми значениями, но TypeScript позволяет нам точно определить, какой тип значения будет возвращён при успешном завершении промиса.
Типизация Promise важна по нескольким причинам:
- Безопасность типов — вы точно знаете, какой тип данных получите
- Автодополнение — IDE подсказывает доступные методы и свойства
- Выявление ошибок — неправильное использование типа будет выявлено на этапе компиляции
- Документация — тип служит живой документацией кода
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.
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>.
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>
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
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 });
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
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. Итоги и лучшие практики
- Всегда указывайте тип generic при создании промиса
- Используйте
Promise<void>для функций без возвращаемого значения - Типизируйте reject — не оставляйте
any - Избегайте
Promise<any>— это антипаттерн - Используйте generic-функции для переиспользования
- Предпочитайте
async/awaitцепочкам.then() - Создавайте типобезопасные обёртки над API
Тест: Типизация Promise
10 вопросов