October 1, 2021·4 min read·engineeringtypescriptpromisesinference

TypeScript 4.5 beta models recursively awaited values

TypeScript 4.5 beta introduces Awaited and uses it to improve Promise inference. Its recursive conditional type follows thenable structure instead of stripping a single Promise wrapper.

An asynchronous API can accept a value that is already available, a promise for that value, or another object exposing compatible completion behavior. The value observed after awaiting it has a simpler shape than that input union. The TypeScript 4.5 beta announced on October 1 introduces Awaited to model that relationship. It also uses the new utility to improve inference for promise operations.

The motivating problem is not limited to removing one Promise wrapper from a type. Awaiting follows nested asynchronous completion, and APIs such as Promise.all need to describe the values that remain after that process. A shallow transformation can retain promise types that do not belong in the resolved result.

I value this improvement because asynchronous interfaces are common throughout the TypeScript web ecosystem. Precise result types let consumers use the values that the runtime operation actually produces. Assertions that merely erase an unwanted promise type can conceal whether the declaration and implementation agree.

Awaited follows the resolved value recursively

The beta announcement gives nested promises and unions as examples. Awaited<Promise<string>> produces string, while applying it to a nested promise continues until the contained type no longer needs unwrapping. A union containing an ordinary value and a promise distributes through that transformation.

type TextResult = Awaited<Promise<string>>
type NestedResult = Awaited<Promise<Promise<number>>>
type MixedResult = Awaited<boolean | Promise<number>>

// TextResult is string.
// NestedResult is number.
// MixedResult is boolean | number.

These are type expressions, not requests to start asynchronous work. They describe the value an awaiting operation can observe. Computing a type does not execute a promise, resolve an external operation or handle a rejection.

The nested example also illustrates the difference between writing a type and constructing a particular runtime object. Its purpose is to show the recursive transformation. An application should not infer from that spelling that every promise-producing operation exposes another promise as its final fulfillment value.

The declaration examines thenable structure

The beta package's library declaration defines Awaited as a conditional type. It preserves null and undefined through a dedicated branch. It then checks for an object with a then method whose fulfillment parameter can be inspected.

If that parameter is callable, the declaration infers the type of the value it accepts and applies Awaited again. If the inspected fulfillment parameter is not callable, the branch produces never. Values outside the thenable branch remain their original type.

This is structural analysis. A type does not have to be spelled as the built-in Promise for its completion behavior to matter. That is why simply matching Promise<infer T> once would fail to express the full purpose of the utility.

The recursive call also makes the stopping condition important. The utility continues through supported completion structure and returns a nonthenable value type when it reaches one. A declaration author can therefore use it to describe APIs whose inputs include several asynchronous representations without enumerating a fixed nesting depth.

Promise.all needs the relationship for each input

The announcement motivates the utility with an API returning a value, a Promise or a PromiseLike. Earlier inference could leave promise types inside the tuple returned after awaiting Promise.all. That result rejected a return annotation containing only the resolved numbers, even though the operation was intended to produce them.

The beta combines Awaited with other inference behavior to improve that result. Each input position can contribute its awaited value type. The resulting tuple can retain positional information while discarding asynchronous wrappers that the runtime operation has already followed.

This matters for heterogeneous groups as well as repeated numeric inputs. Consumers need to know which result belongs to each position, and whether that result is still an asynchronous object. Broadening the whole result to an unconstrained array would avoid some errors while losing that relationship.

The fix therefore improves the description of an existing operation. It does not change the scheduling or rejection behavior of Promise.all. A more accurate inferred result reduces static friction without making the underlying asynchronous work more reliable by itself.

Recursive types still need a resource bound

The same beta includes tail-recursion elimination for some conditional types. The announcement explains that the compiler retains heuristics against excessive or potentially infinite instantiation. Conditional branches that immediately return another conditional type can avoid some intermediate instantiations.

That optimization is relevant to recursive utilities generally, but it does not promise unlimited evaluation for every recursive type. Building a union from a recursive result has different intermediate requirements from returning the recursive result directly. The declaration's form affects the work required of the checker.

I prefer a standard utility here because libraries can share one description of awaiting behavior. Consumers no longer need several shallow promise-unwrapping aliases with subtly different limits. During the beta, the useful comparison is whether a declaration produces the resolved value types expected from the actual asynchronous operation, including union inputs and compatible thenable objects.