TypeScript Type Challenge Awaited Walkthrough
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;