- Главная
- Typescript
- Promise в TypeScript: типизация значений
Promise в TypeScript: типизация значений
Promise — один из главных инструментов асинхронного JavaScript, но сам по себе он не сообщает, какое именно значение окажется внутри после успешного выполнения. В этом уроке разбираемся, как TypeScript решает эту проблему с помощью типизации промисов через угловые скобки. Вы увидите, как правильно указывать тип для Promise, ловить ошибки ещё на этапе компиляции и безопасно работать с несколькими асинхронными операциями одновременно.
Зачем указывать тип значения внутри Promise
Когда функция возвращает Promise, важно понимать не только сам факт асинхронности, но и то, каким будет итоговое значение после выполнения операции. В обычном JavaScript это остаётся неявным: разработчику приходится держать в голове или искать в документации, что именно вернёт запрос к серверу, чтение файла или таймер. Если тип значения не указан явно, TypeScript будет вынужден либо выводить его самостоятельно из кода, либо, в худшем случае, присвоит переменной тип any, что полностью отключает проверку типов для всего, что происходит с этим значением дальше.
Явная типизация промисов решает сразу несколько задач. Во-первых, она документирует контракт функции: любой, кто вызывает эту функцию, сразу видит, что получит в ответ. Во-вторых, она позволяет компилятору проверять код внутри функции — например, убедиться, что вы вызываете resolve со значением подходящего типа. В-третьих, она защищает код, который использует результат промиса, от опечаток и обращений к несуществующим свойствам.
Синтаксис Promise с угловыми скобками
TypeScript использует угловые скобки, чтобы указать, какой тип значения находится внутри промиса. Сам тип Promise является обобщённым (generic), то есть принимает параметр типа, который описывает результат асинхронной операции. Базовый синтаксис выглядит так: Promise<T>, где T — это тип, который будет передан в resolve.
function loadMessage(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve("Привет из будущего"), 1000);
});
}
В этом примере функция loadMessage явно объявлена так, что она возвращает Promise со строкой внутри. Если внутри промиса вызвать resolve с числом или объектом вместо строки, TypeScript сразу же сообщит об ошибке несоответствия типов, не дожидаясь запуска кода.
Что происходит, если тип не указан явно
TypeScript умеет выводить тип возвращаемого значения автоматически, анализируя, с чем вызывается resolve внутри функции. Такой вывод типов часто работает достаточно точно для простых случаев, поэтому необязательно писать аннотацию буквально везде. Но по мере роста функции — когда внутри появляются условные ветки, ранние возвраты или сложная обработка ошибок — вывод типов может стать менее очевидным и точным. Поэтому хорошей практикой считается явно указывать тип для публичных функций и методов, даже если TypeScript способен вывести его сам: это делает код более читаемым и предсказуемым для других разработчиков.
Типизация функций, возвращающих Promise
Чаще всего с промисами приходится работать через ключевые слова async и await, а не через прямое создание объекта Promise. Хорошая новость в том, что типизация в этом случае выглядит точно так же: возвращаемый тип асинхронной функции всё равно указывается как Promise<T>, даже если внутри функции нет ни одного явного упоминания слова Promise.
interface User {
id: number;
name: string;
email: string;
}
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: User = await response.json();
return data;
}
Здесь функция fetchUser объявлена так, что она обязательно вернёт User, обёрнутого в Promise. Обратите внимание, что тип указан именно как Promise<User>, а не просто User, потому что функция помечена как async и всегда возвращает промис, а не готовое значение напрямую. Если внутри функции по ошибке вернуть значение неподходящего типа, компилятор укажет на конкретную строку с проблемой ещё до того, как код попадёт в продакшен.
Используйте встроенный утилитный тип Awaited<T>, чтобы получить тип значения внутри Promise без ручного дублирования интерфейса: type Data = Awaited<ReturnType<typeof fetchUser>> вернёт именно User, даже если сигнатура функции изменится в будущем.
Как TypeScript ловит ошибки типизации в Promise
Главное преимущество типизированных промисов раскрывается именно тогда, когда в коде допущена ошибка. Без явного указания типа несоответствие могло бы остаться незамеченным до момента выполнения кода в браузере или на сервере. С типизацией же компилятор сообщает о проблеме сразу.
function loadCount(): Promise<number> {
return new Promise((resolve) => {
resolve("42");
});
}
В этом примере функция объявлена так, что должна возвращать Promise<number>, но внутри в resolve передаётся строка "42". TypeScript немедленно укажет на несовпадение типов: аргумент типа string нельзя присвоить параметру типа number. Такая проверка особенно ценна в больших проектах, где функция объявляется в одном файле, а используется совсем в другом — без явной типизации подобная ошибка могла бы всплыть только в рантайме, причём в самом неожиданном месте.
Обработка ошибок в типизированных Promise
Промисы могут не только успешно выполниться, но и завершиться с ошибкой. При работе с async/await для этого используется обычная конструкция try/catch. Важный нюанс в строгом режиме TypeScript: значение, попавшее в блок catch, по умолчанию имеет тип unknown, а не any, поэтому перед использованием его нужно сузить.
async function safeLoadUser(id: number): Promise<User | null> {
try {
return await fetchUser(id);
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
return null;
}
}
Возвращаемый тип здесь объявлен как Promise<User | null> — union из двух вариантов: либо успешно загруженный пользователь, либо null в случае ошибки. Такая типизация заставляет каждого, кто вызывает эту функцию, явно обработать оба возможных исхода, а не забыть про случай неудачи.
Работа с несколькими Promise одновременно
Реальные приложения редко ограничиваются одним асинхронным запросом. TypeScript корректно типизирует и встроенные статические методы для работы с группами промисов, что позволяет безопасно комбинировать несколько источников данных.
| Метод | Тип результата |
|---|---|
| Promise.all | Promise<[T1, T2, …]> |
| Promise.race | Promise<T1 | T2 | …> |
| Promise.allSettled | Promise<PromiseSettledResult<T>[]> |
| Promise.any | Promise<T1 | T2 | …> |
Promise.all дожидается выполнения всех переданных промисов и возвращает кортеж с результатами в том же порядке, при этом TypeScript сохраняет для каждого элемента его собственный тип, а не сводит всё к общему объединению.
async function loadDashboard(userId: number) {
const [user, posts] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
]);
return { user, posts };
}
В этом примере user получит тип User, а posts — тип массива постов, хотя оба значения были получены из одного вызова Promise.all. TypeScript автоматически сопоставляет позицию каждого промиса в массиве с его типом результата, поэтому дополнительная ручная типизация здесь не требуется.
Не оборачивайте уже асинхронную функцию в дополнительный Promise вручную — если функция объявлена как async, TypeScript сам разворачивает вложенные промисы благодаря автоматическому «сплющиванию» типов, поэтому конструкция вроде Promise<Promise<User>> практически никогда не нужна и обычно говорит об ошибке в коде.
Итоги урока
- Тип
Promise<T>указывает, каким будет значение после успешного выполнения асинхронной операции. - Угловые скобки задают точный тип результата, который TypeScript проверяет уже на этапе компиляции.
- Компилятор ловит несоответствия типов как внутри вызова
resolve, так и в возвращаемых значенияхasync-функций. - Значение в блоке
catchпо умолчанию имеет типunknownи требует сужения перед использованием. - Для работы с несколькими промисами одновременно используйте типизированные методы
Promise.all,Promise.race,Promise.allSettledиPromise.any.
Что дальше
На следующем уроке речь пойдёт об обобщённых типах — generics: вы узнаете, как создавать переиспользуемые типы и функции, где конкретный тип задаётся не заранее, а в момент использования.