Асинхронность и промисы
Типизация асинхронного кода — сигнатуры колбэков против Promise<T>, как async/await выводит тип возврата, Awaited<T>, Promise.all над разнородными промисами и асинхронные генераторы.
7 вопросов
MiddleТеорияОчень частоКак TypeScript выводит тип возврата async-функции?
Как TypeScript выводит тип возврата async-функции?
Это всегда Promise: возврат T выводится как Promise<T>, и явная аннотация тоже обязана быть Promise<T>, а не голым T. await разворачивает его рекурсивно, поэтому await над Promise<Promise<string>> всё равно даёт string. Цепочка .then выводится так же, ведь then схлопывает thenable, а не вкладывает их.
Типичные ошибки
- ✗Аннотировать
async-функцию какTвместоPromise<T> - ✗Считать, что
awaitразворачивает только один уровень вложенности - ✗Думать, что цепочка
.thenвкладывает промисы, а не схлопывает их
Уточняющие вопросы
- →Что будет, если
async-функция вернёт thenable, который не являетсяPromise? - →Почему
async-функция, ничего не возвращающая, выводится какPromise<void>?
JuniorТеорияЧастоКак типизировать API на колбэках и чем от него отличается API на промисах?
Как типизировать API на колбэках и чем от него отличается API на промисах?
Callback-API объявляет результат в параметрах колбэка — обычно (err: Error | null, data?: T) => void, — а сама функция возвращает void. Promise-API возвращает Promise<T>, поэтому результат — это аргумент типа. Ошибку не типизирует ни один стиль: reject — это any или unknown.
Типичные ошибки
- ✗Ждать, что
Promise<T>объявит тип ошибки, как вPromise<T, E> - ✗Типизировать
errколбэка какError, хотя при успехе онnull - ✗Думать, что результат callback-API виден в его собственном типе возврата
Уточняющие вопросы
- →Как типизировать Node-хелпер преобразования
promisifyдля такого callback-API? - →Почему пойманный reject получает тип
unknownприuseUnknownInCatchVariables?
MiddleКодЧастоТипизация хелпера, принимающего либо значение, либо Promise от него
Типизация хелпера, принимающего либо значение, либо Promise от него
Принимайте T | Promise<T>, а отдавайте Promise<T>. Внутри async-функции await input приводит обе формы к T, и даже голый return input проходит проверку, ведь позиция возврата у async и так принимает значение или thenable. Чего делать нельзя — выпускать T | Promise<T> наружу: объединение в возврате заставит каждого вызывающего ветвиться, а Promise<T> позволит просто сделать await.
Типичные ошибки
- ✗Выпускать
T | Promise<T>наружу как тип возврата, перекладывая ветвление на вызывающих - ✗Считать, что
awaitнад не-промисом — ошибка, а не пустая операция - ✗Ждать, что
async-функция дважды обернёт возвращаемый ею промис
Уточняющие вопросы
- →Как
Awaited<T>меняет эту сигнатуру, еслиTсам может бытьPromise? - →Почему
return inputпроходит проверку, хотяinputможет и не бытьPromise?
SeniorКодЧастоОбобщённая обёртка retry с бэкоффом, сохраняющая сигнатуру обёрнутой функции
Обобщённая обёртка retry с бэкоффом, сохраняющая сигнатуру обёрнутой функции
Сделайте обёртку обобщённой по кортежу параметров и результату: withRetry<A extends unknown[], R>(fn: (...args: A) => Promise<R>, …): (...args: A) => Promise<R>. Вывод типов заполняет A и R по месту вызова, поэтому обёртка сохраняет точную сигнатуру без any и без приведений. Тело крутит цикл attempts раз, ждёт вызов в try, засыпает на baseMs * 2 ** (i - 1) при провале и пробрасывает последнюю ошибку.
Типичные ошибки
- ✗Скатываться к
any[]/Functionвместо обобщённого кортежа параметровA extends unknown[] - ✗Проглатывать итоговый провал — возвращать
undefinedвместо проброса последней ошибки - ✗Засыпать перед первой попыткой, из-за чего самый первый вызов задерживается зря
Уточняющие вопросы
- →Как добавить предикат
shouldRetry(err: unknown): boolean, не ослабив типы? - →Почему
A extends unknown[]здесь безопаснее как ограничение, чемA extends any[]?
MiddleТеорияИногдаЗачем нужен утилитный тип Awaited<T>, а не T extends Promise<infer U> ? U : T?
Зачем нужен утилитный тип Awaited<T>, а не T extends Promise<infer U> ? U : T?
Awaited<T> разворачивает рекурсивно: он снимает слои, пока тип остаётся thenable, поэтому Awaited<Promise<Promise<string>>> — это string. Написанный вручную условный тип снимает ровно один слой и оставляет Promise<string>. Awaited<T> ещё и дистрибутивен по объединениям и пропускает не-промисы как есть, повторяя поведение await.
Типичные ошибки
- ✗Думать, что
Awaited<T>снимает только один уровень, как ручной условный тип - ✗Считать, что
Awaited<T>что-то делает в рантайме, а не стирается полностью - ✗Ждать, что
Awaited<T>отвергнет не-промис, вместо того чтобы пропустить его
Уточняющие вопросы
- →Что даст
Awaited<T>для своего объекта, у которого просто есть методthen? - →Где
Awaitedпоявляется в выводимом типе результатаPromise.all?
MiddleТеорияИногдаЧем async function* отличается от обычного генератора и как его потреблять?
Чем async function* отличается от обычного генератора и как его потреблять?
Обычный генератор возвращает Generator<Y, R, N> и отдаёт значения синхронно. async function* возвращает AsyncGenerator<Y, R, N>, у которого next() даёт Promise<IteratorResult<Y, R>>, поэтому тело может делать await между yield. Потребляют его через for await (const x of gen); обычный for...of вообще не сможет его обойти.
Типичные ошибки
- ✗Ждать от
next()обычныйIteratorResult, а неPromiseот него - ✗Пытаться обойти асинхронный генератор через
for...ofвместоfor await - ✗Думать, что
async function*возвращаетPromise<Generator<...>>
Уточняющие вопросы
- →Что означают три параметра типа у
AsyncGenerator<Y, R, N>? - →Как типизировать объект, асинхронно итерируемый через
Symbol.asyncIterator?
MiddleКодИногдаТипизация Promise.all над кортежем разнородных промисов
Типизация Promise.all над кортежем разнородных промисов
У Promise.all есть перегрузка для кортежа, которая прогоняет каждую позицию через Awaited<T[P]>, поэтому каждый слот сохраняет свой тип. Она срабатывает, только пока аргумент остаётся кортежем: в обычной const-переменной элементы расширяются до (Promise<User> | Promise<number> | Promise<boolean>)[], и результат схлопывается в массив объединения. Сохраните кортеж через as const или передайте литерал массива прямо в вызов.
Типичные ошибки
- ✗Считать, что
const-массив промисов остаётся кортежем, а не расширяется до массива - ✗Хвататься за
as [User, number, boolean]вместо сохранения вывода кортежа - ✗Думать, что
Promise.allв принципе может дать только массив объединения
Уточняющие вопросы
- →Как
Promise.allSettledменяет тип элементов получившегося кортежа? - →Почему литерал массива, переданный в
Promise.allнапрямую, уже выводится как кортеж?