Menu

Promise в TypeScript: Promise<T>, Promise.all и allSettled

Как типизируются промисы в TypeScript: тип Promise<T>, типизация new Promise и resolve, как then меняет тип, почему catch даёт any, Promise.all с результатом-кортежем, типы результатов Promise.allSettled и обёртка API на колбэках в типизированный промис.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

В TypeScript у промиса тип Promise<T>, где T это тип значения, которым он разрешается. Функция, которая возвращает промис, объявляет это в типе возврата, а значение, полученное из then или await, имеет тип T.

Механика промисов (состояния, очередь микрозадач, цепочки) это обычный JavaScript, описанный в разделе промисы в JavaScript. Эта страница о типах.

Типизация new Promise и resolve

Когда new Promise это возвращаемое значение функции с объявленным типом возврата, T берётся оттуда. Сам по себе TypeScript не выводит T из вызовов resolve, и вы получаете Promise<unknown>. Передайте аргумент типа явно:

Две детали. resolve типизирован по T, поэтому resolve("42") в Promise<number> это ошибка компиляции. А resolve() без аргумента разрешён, только если T включает void: в Promise<number> это ошибка TS2794 (Expected 1 arguments, but got 0. Did you forget to include 'void' in your type argument to 'Promise'?).

Сторона reject не типизирована. Типа Promise<T, E> нет: промис может отклониться с любым значением.

then, catch и finally

Каждый then возвращает новый промис, типизированный тем, что возвращает его колбэк. Если колбэк возвращает промис, TypeScript его разворачивает, поэтому Promise<Promise<T>> вы никогда не получите.

Параметр колбэка catch в стандартной библиотеке типизирован как any, а не unknown. Ничто не мешает написать e.message для значения, которое может оказаться строкой. Аннотируйте его как unknown, как выше, и сужайте перед использованием. try/catch вокруг await справляется лучше: при strict его переменная catch уже имеет тип unknown (см. обработку ошибок).

Promise.all возвращает кортеж

Для литерала массива из промисов разных типов Promise.all возвращает промис кортежа, где каждая позиция сохраняет свой тип:

Значения, не являющиеся промисами, можно подмешивать, и они проходят без изменений. Promise.all отклоняется, как только отклоняется один вход, и остальные результаты теряются. Promise.race разрешается или отклоняется с первым завершившимся и типизирован как объединение входов; Promise.any разрешается первым успехом и отклоняется с AggregateError, только если провалились все входы.

Promise.allSettled и тип его результата

Promise.allSettled ждёт каждый вход и никогда не отклоняется. Каждый результат это PromiseSettledResult<T>, объединение, которое сужается по полю status:

r.value существует только после проверки status, потому что у отклонённого варианта нет value. reason имеет тип any по той же причине, что и в catch. filter использует предикат типа, чтобы отфильтрованный массив был типизирован как выполненные результаты.

Обёртка API на колбэках в промис

Старые API сообщают результаты через колбэк, часто в стиле Node (err, result) => void. Оберните их один раз в функцию, которая возвращает типизированный промис, и остальной код сможет использовать await:

В Node это делает util.promisify для функций, следующих соглашению (err, result), а у многих встроенных модулей уже есть версии на промисах (node:fs/promises, node:timers/promises).

Частые ошибки

  • Возвращать T там, где сигнатура говорит Promise<T>, или наоборот. Не-async функция, объявленная как Promise<User>, должна возвращать промис; функция async возвращает его автоматически.
  • Забыть обработать промис. Вызов вроде save(user); без await, then или catch прекрасно компилируется, а отклонение становится необработанным (по умолчанию это завершает процесс Node). Такие места ловит правило typescript-eslint no-floating-promises.
  • Доверять типу .catch((e) => ...). Его e имеет тип any. Аннотируйте его как unknown.
  • Оборачивать в new Promise то, что уже возвращает промис. Просто верните существующий промис или дождитесь его через await.

Часто задаваемые вопросы

Что такое Promise<T> в TypeScript?

Promise<T> это тип промиса, который разрешается значением типа T. Функция, возвращающая Promise<string>, отдаёт промис, чей колбэк then или await даёт string. Промис, который разрешается без значения, это Promise<void>.

Как типизировать new Promise в TypeScript?

Передайте аргумент типа: new Promise<number>((resolve, reject) => ...). Без него TypeScript не может вывести значение из вызовов resolve, и результатом будет Promise<unknown>. Для промиса, который разрешается ничем, используйте new Promise<void>(resolve => ...), чтобы разрешить resolve() без аргумента.

Какого типа ошибка в catch промиса?

any. Параметр reason колбэка catch в стандартной библиотеке типизирован как any, потому что выбросить или отклонить можно что угодно. Аннотируйте его сами как unknown (.catch((e: unknown) => ...)) и сужайте через instanceof Error перед использованием.

Как Promise.all работает с типами в TypeScript?

Promise.all для литерала массива возвращает тип кортежа с одним элементом на каждый вход, по порядку: Promise.all([getUser(), getCount()]) это Promise<[User, number]>, поэтому деструктуризация даёт каждому значению собственный тип. Для массива одного типа T[] он возвращает Promise<T[]>.

Чем Promise.all отличается от Promise.allSettled?

Promise.all отклоняется, как только отклоняется любой вход. Promise.allSettled всегда разрешается массивом объектов PromiseSettledResult<T>: { status: "fulfilled", value } или { status: "rejected", reason }. Проверяйте status, чтобы сузить каждый из них.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ