TypeScript Type Challenge Awaited Walkthrough

| 2 min read

The goal of the challenge is to implement a generic type Awaited that returns the type of something wrapped in a Promise.

The challenge: https://github.com/type-challenges/type-challenges/blob/main/questions/00189-easy-awaited/README.md

Watch the video

Update - new test cases added

A new test case has been added since I tackled this challenge:

type T = { then: (onfulfilled: (arg: number) => any) => any }

type cases = [
Expect<Equal<MyAwaited<T>, number>>,
]

This is a "promise like" type, without actually being a Promise. Fortunately, TypeScript has a PromiseLike interface covering this. We can replace Promise with PromiseLike in our below type and everything works!

An example case

For example, given the following cases, our Awaited type should return the type as shown in the promise:

 MyAwaited<Promise<string> // string
MyAwaited<Promise<{ field: number }> // { field: number }
MyAwaited<Promise<Promise<string | number>> // string | number
MyAwaited<Promise<Promise<Promise<string | boolean>>> // string | boolean

The first two examples show a simple promise type being passed to MyAwaited with the type inside that Promise being returned. The third and fourth examples show that our type needs to work with nested promises.

Approach

Lets consider only the first example case above:

  MyAwaited<Promise<string> // string

We could write a type to resolve this to a string like so:

type MyAwaited<T> = T extends Promise<string> ? string : never;

In english this reads:

if you pass a promise of a string, return a string, otherwise return never

This only works for the first example case, but we could extend it to work for the second case. In english we could say:

if you pass a promise of a string, return a string, otherwise, if you pass a promise of an object type, return that object type, otherwise return never

We can extend our type to handle multiple cases:

type MyAwaited<T> = T extends Promise<string>
? string
: T extends Promise<{ field: number }>
? { field: number }
: never;

This would now work for the first two example cases, and could be extended indefinitely to handle all cases. But if we took this approach to handle all cases, our type would become infinitely long. Instead we need a generic solution to this problem.

The solution

When working with conditional types in TypeScript, we can make use of inference within conditional types.

Instead of extending our type, we can replace it with a generic version making use of the infer keyword:

type MyAwaited<T> = T extends Promise<infer InnerType> ? InnerType : never;

This works for the first two example cases, in the first example InnerType takes on the type string and returns that, in the second example it takes on the type {field: number} and we return that.

This still doesn't solve the nested cases. At present, when we pass Promise<Promise<string | number>> to our type, we currently get back Promise<string | number>, i.e. it unwraps one level of Promises. We need to unwrap all the promises!

We can extend our type to work recursively:

type MyAwaited<T> = T extends Promise<infer InnerType>
? InnerType extends Promise<any>
? MyAwaited<InnerType>
: InnerType
: never;

Here we check if InnerType is still a promise (after being unwrapped once as per before), if it is, we call MyAwaited on it again (the recursive call). If InnerType is no longer a promise, we return it's type.

Finally, we need to constrain the type T so that we can only pass Promise types to our type, and our final type looks like:

type MyAwaited<T extends Promise<any>> = T extends Promise<infer InnerType>
? InnerType extends Promise<any>
? MyAwaited<InnerType>
: InnerType
: never;