Async & Promises
Typing asynchronous code — callback signatures versus Promise<T>, how async/await infers return types, Awaited<T>, Promise.all over heterogeneous promises, and async generators.
7 questions
MiddleTheoryVery commonHow does TypeScript infer the return type of an async function?
How does TypeScript infer the return type of an async function?
It is always a Promise: returning T infers Promise<T>, and an explicit annotation must be Promise<T> too — never a bare T. await unwraps it recursively, so awaiting Promise<Promise<string>> still gives string. A .then chain infers the same way, because then flattens thenables rather than nesting them.
Common mistakes
- ✗Annotating an
asyncfunction withTinstead ofPromise<T> - ✗Believing
awaitunwraps only a single layer of nesting - ✗Thinking a
.thenchain nests promises instead of flattening them
Follow-up questions
- →What happens when an
asyncfunction returns a thenable that is not aPromise? - →Why does an
asyncfunction that returns nothing inferPromise<void>?
JuniorTheoryCommonHow do you type a callback-based API versus a Promise-based one?
How do you type a callback-based API versus a Promise-based one?
A callback API declares its result in the callback's parameters — typically (err: Error | null, data?: T) => void — and itself returns void. A Promise API returns Promise<T>, so the result is the type argument. Neither style types the failure: a rejection is any or unknown.
Common mistakes
- ✗Expecting
Promise<T>to declare its rejection type, as inPromise<T, E> - ✗Typing the callback's
errasErrorwhen on success it isnull - ✗Thinking a callback API's result shows up in its own return type
Follow-up questions
- →How would you type the Node-style conversion helper
promisifyfor such a callback API? - →Why is a caught rejection typed
unknownunderuseUnknownInCatchVariables?
MiddleCodeCommonTyping a helper that accepts either a value or a Promise of it
Typing a helper that accepts either a value or a Promise of it
Take T | Promise<T> in and hand Promise<T> back. Inside an async function await input normalizes both shapes to T, and even a bare return input type-checks, because an async return position already accepts a value or a thenable. What you must not do is leak T | Promise<T> outward — a union return forces every caller to branch, while Promise<T> lets them simply await.
Common mistakes
- ✗Letting
T | Promise<T>escape as the return type, pushing the branch onto callers - ✗Believing
awaiton a non-promise value is an error rather than a no-op - ✗Expecting an
asyncfunction to double-wrap a promise it returns
Follow-up questions
- →How does
Awaited<T>change this signature whenTmay itself be aPromise? - →Why does
return inputtype-check even thoughinputmay not be aPromise?
SeniorCodeCommonA generic retry-with-backoff wrapper that preserves the wrapped function's signature
A generic retry-with-backoff wrapper that preserves the wrapped function's signature
Make the wrapper generic over the parameter tuple and the result: withRetry<A extends unknown[], R>(fn: (...args: A) => Promise<R>, …): (...args: A) => Promise<R>. Inference fills A and R from the call site, so the wrapper keeps the exact signature with no any and no assertions. The body loops attempts times, awaits the call inside try, sleeps baseMs * 2 ** (i - 1) on failure, and rethrows the last error.
Common mistakes
- ✗Falling back to
any[]/Functioninstead of a generic parameter tupleA extends unknown[] - ✗Swallowing the final failure — returning
undefinedrather than rethrowing the last error - ✗Sleeping before the first attempt, so the very first call is needlessly delayed
Follow-up questions
- →How would you add a
shouldRetry(err: unknown): booleanpredicate without loosening the types? - →Why is
A extends unknown[]a safer constraint here thanA extends any[]?
MiddleTheoryOccasionalWhy use the utility type Awaited<T> rather than T extends Promise<infer U> ? U : T?
Why use the utility type Awaited<T> rather than T extends Promise<infer U> ? U : T?
Awaited<T> unwraps recursively: it keeps peeling while the type is still a thenable, so Awaited<Promise<Promise<string>>> is string. The hand-written conditional strips exactly one layer and leaves Promise<string>. Awaited<T> also distributes over unions and passes non-promise types straight through, matching what await really does.
Common mistakes
- ✗Thinking
Awaited<T>unwraps only one level, like the hand-written conditional - ✗Assuming
Awaited<T>has a runtime effect rather than being fully erased - ✗Expecting
Awaited<T>to reject a non-promise type instead of passing it through
Follow-up questions
- →What does
Awaited<T>produce for a custom object that merely has athenmethod? - →Where does
Awaitedshow up in the inferred result type ofPromise.all?
MiddleTheoryOccasionalHow does async function* differ from a regular generator, and how do you consume it?
How does async function* differ from a regular generator, and how do you consume it?
A regular generator returns Generator<Y, R, N> and yields synchronously. An async function* returns AsyncGenerator<Y, R, N>, whose next() gives a Promise<IteratorResult<Y, R>>, so the body may await between yields. You consume it with for await (const x of gen); a plain for...of cannot iterate it at all.
Common mistakes
- ✗Expecting
next()to return a plainIteratorResultrather than aPromiseof one - ✗Trying to consume an async generator with
for...ofinstead offor await - ✗Thinking
async function*returnsPromise<Generator<...>>
Follow-up questions
- →What do the three type parameters of
AsyncGenerator<Y, R, N>stand for? - →How would you type an object that is async-iterable via
Symbol.asyncIterator?
MiddleCodeOccasionalTyping Promise.all over a heterogeneous tuple of promises
Typing Promise.all over a heterogeneous tuple of promises
Promise.all has a tuple overload that maps each position through Awaited<T[P]>, so every slot keeps its own type. It only fires while the argument is still a tuple: stored in a plain const array the elements widen to (Promise<User> | Promise<number> | Promise<boolean>)[], collapsing the result to a union array. Keep the tuple with as const, or pass the array literal inline.
Common mistakes
- ✗Assuming a
constarray of promises stays a tuple instead of widening to an array - ✗Reaching for
as [User, number, boolean]instead of preserving tuple inference - ✗Thinking
Promise.allcan only ever produce an array of a union
Follow-up questions
- →How does
Promise.allSettledchange the element type of the resulting tuple? - →Why does passing the array literal directly to
Promise.allalready infer a tuple?