The challenge is to recreate the TypeScript builtin type Awaited
which unwraps a type from a Promise or nested Promises.
To solve this, we'll use a TypeScript feature known as conditional types. Conditional types allow us to choose the type based on a condition. They can be used to extract types wrapped in other types, such as promises.
type SomeType = boolean extends true ? string : number
let someVariable: SomeType
In the above example, SomeType
will always be of type number
because boolean extends true
is false
. This is because a conditional type in the form: TypeA extends TypeB
, checks whether TypeA
is a subtype of TypeB
. boolean
is not a subtype of true
, it's the opposite, true
is a subtype of boolean
.
To create an Awaited
type, we first need to understand that Promises in TypeScript can be represented as Promise<T>
, where T
is the type of the value that the promise resolves to.
We can use conditional types to unwrap the Promise type:
type MyAwaited<T> = T extends Promise<infer U> ? U : T
The conditional type T extends Promise<infer U> ? U : T
can be read as follows:
- If
T
can be assigned toPromise<U>
for someU
(i.e.,T
extendsPromise<U>
), then the result isU
. - Otherwise, if
T
cannot be assigned to anyPromise<U>
, then the result isT
itself.
The infer
keyword is used in conditional types to infer the type that is being extended. Here it's used to infer the type U
that Promise<U>
wraps.
Let's consider a few examples:
-
If
T
isPromise<string>
, thenU
is inferred to bestring
. So,T extends Promise<infer U> ? U : T
will resolve tostring
.type T = Promise<string> type Result = T extends Promise<infer U> ? U : T // Result is string
-
If
T
isPromise<number>
, thenU
is inferred to benumber
. So,T extends Promise<infer U> ? U : T
will resolve tonumber
.type T = Promise<number> type Result = T extends Promise<infer U> ? U : T // Result is number
-
If
T
isstring
(not a Promise), thenT
does not extendPromise<U>
for anyU
, soT extends Promise<infer U> ? U : T
will resolve toT
, which isstring
.type T = string type Result = T extends Promise<infer U> ? U : T // Result is string
This allows us to "unwrap" the type that a Promise resolves to, or leave the type as-is if it's not a Promise.
To handle nested Promises, we add a recursive step. We reapply MyAwaited
to U
if U
itself extends Promise<something>
.
Here's the updated recursive solution:
type MyAwaited<T> = T extends Promise<infer U>
? U extends Promise<infer V>
? MyAwaited<V>
: U
: T
For better handling of the PromiseLike
objects (including nested Promise), we will improve our solution:
type MyAwaited<T extends PromiseLike<any | PromiseLike<any>>> =
T extends PromiseLike<infer U>
? U extends PromiseLike<any>
? MyAwaited<U>
: U
: never
In this solution, MyAwaited
receives a type T
that must extend PromiseLike<any | PromiseLike<any>>
. Then, it checks if T
is PromiseLike
of some type U
. If U
is itself PromiseLike<any>
, the function re-applies MyAwaited
to U
recursively. If U
is not PromiseLike<any>
, U
is returned as the result. If T
doesn't match the requirements, never
is returned.