type-challenges-solutions icon indicating copy to clipboard operation
type-challenges-solutions copied to clipboard

type-challenges-solutions/en/easy-awaited

Open utterances-bot opened this issue 3 years ago • 18 comments

Awaited

This project is aimed at helping you better understand how the type system works, writing your own utilities, or just having fun with the challenges.

https://ghaiklor.github.io/type-challenges-solutions/en/easy-awaited.html

utterances-bot avatar Apr 26 '21 12:04 utterances-bot

I did this one many times and tried a couple of different variations to it. I believe we could improve this even further, to warn us if we pass something other than a Promise.

With that said, if we were to improve it with that safety implemented, we would not return T.

Here's my suggestion:

type Awaited<T extends Promise<any>> = T extends Promise<infer R> ? R : never;

So now if we were to do something like:

type Inferred = Awaited<'string'>;
//                       ^ Type 'string' does not satisfy the constraint 'Promise<any>'.

I learned this from Maciej Sikora, so credits to him!

dvlden avatar Apr 26 '21 12:04 dvlden

@dvlden true, by using generic constraints we can narrow the expected type to not allow anything but Promise.

I was thinking from the different angle, when solving the challenge. I thought, why not to return input type as is if it was not a promise. Like, the result is already there and we don't need to wait for it.

For others, if you want to narrow the input type to be only a Promise then follow @dvlden solution. If you want to optionally unwrap the promise then don't add a constraint.

ghaiklor avatar Apr 26 '21 13:04 ghaiklor

The solution originally provided

type Awaited<T> = T extends Promise<infer R> ? R : T;

does not satisfy the following two tests:

Expect<Equal<MyAwaited<Z>, string | number>>, //recursion

and the one which expects error:

// @ts-expect-error type error = MyAwaited<number> (Also, the type in solution is misnamed, it has to be MyAwaited, not Awaited)

The correct solution, which satisfies both these tests, as well as deeper recursion levels is the following:

type MyAwaited<P extends Promise<unknown>> = P extends Promise<infer T> ? T extends Promise<unknown> ? MyAwaited<T> : T : P

pragmasoft-ua avatar Dec 14 '21 12:12 pragmasoft-ua

@pragmasoft-ua seems like they have added more tests to the challenge. Can you elaborate why the T extends Promise<unknown> check? It seems to me that we could just recursively call the type itself, no?

ghaiklor avatar Dec 15 '21 08:12 ghaiklor

Can you elaborate why the T extends Promise<unknown> check? It seems to me that we could just recursively call the type itself, no?

This exactly happens when we do

T extends Promise<unknown> ? MyAwaited<T>

That is, we unwrap outermost of the nested promises and evaluate our type MyAwaited recursively. Returning T in another branch is a recursion termination condition.

In cases when we do not return MyAwaited there's no recursion. For recursion we need to evaluate the same type again.

I tried a test with triple promise nesting and it worked.

pragmasoft-ua avatar Dec 15 '21 11:12 pragmasoft-ua

@pragmasoft-ua I meant your T extends Promise<unknown>. There is no sense in adding one more conditional type to what we already have. Why not T extends Promise<infer R> ? Awaited<R> : T?

ghaiklor avatar Dec 19 '21 15:12 ghaiklor

@pragmasoft-ua I meant your T extends Promise<unknown>. There is no sense in adding one more conditional type to what we already have. Why not T extends Promise<infer R> ? Awaited<R> : T?

Agree

pragmasoft-ua avatar Dec 22 '21 15:12 pragmasoft-ua

@pragmasoft-ua you could do that only if this test didn't exist:

// @ts-expect-error
type error = MyAwaited<number>

But because it exists, the requirement for T to extend Promise<unknown> would break the recursive call, since the inferred type is not guaranteed to extend Promise. Unless I'm missing something, there's no way around the nested conditional.

DaniGuardiola avatar Apr 01 '22 20:04 DaniGuardiola

My solution:

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

The type is guarded at root to only allow promises T extends Promise<any> as types being passed inside it. The first level conditional T extends Promise<infer R> only exists to infer the type inside the promise, it isn't checking for anything. Also notice how it clearly says never on this condition since it is impossible to be in that arm and we don't allow it. The second conditional R extends Promise<any> checks if R can be further unwrapped, if not then return the value.

S-Mann avatar Apr 27 '22 16:04 S-Mann

type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer R>
  ? R extends Promise<unknown>
    ? MyAwaited<R>
    : R
  : T;

farukEncoded avatar Jul 08 '22 12:07 farukEncoded

Or

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

farukEncoded avatar Jul 08 '22 12:07 farukEncoded

This seems to work:

type MyAwaited<T extends Promise<any>> = T extends Promise<infer R> ? Awaited<R> : never

AshNaz87 avatar Aug 22 '22 22:08 AshNaz87

@AshNaz87 well, it work cause you're using Awaited :D

it doesn't count

stasgavrylov avatar Sep 10 '22 06:09 stasgavrylov

Actually, all the examples above do not satisfy the last one case from TS challenge (it may have been added recently):

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

 Expect<Equal<MyAwaited<T>, number>>,

So my solution is:

type GPromise = { then: (onfulfilled: (...args: any) => any) => any };

type MyAwaited<T> = T extends Promise<infer R> 
  ? R extends Promise<infer L>
    ? MyAwaited<L>
    : R
  : T extends GPromise
    ? Parameters<Parameters<T['then']>[number]>[number]
    : T;

misterhomer1992 avatar Jan 01 '23 22:01 misterhomer1992

I can't believe this challenge is considered easy.

Thanks everybody for the explanations. Thanks @misterhomer1992 for help to pass the last test - perhaps last test was added recently. We can simplify the type a little further because the last type T is satisfied by PromiseLike

type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U> 
  ? U extends PromiseLike<unknown> // Is nested PromiseLike?
    ? MyAwaited<U>                 // Yes, further unwrap type inside PromiseLike
    : U                            // Not PromiseLike, return type
  : never;                         // Type error

AndrewLamWARC avatar Jan 07 '23 11:01 AndrewLamWARC

type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer R> ? Awaited<R> : T

huangzonggui avatar Mar 12 '23 10:03 huangzonggui

I hate <any> types, therefore i wrote this code.

type MyAwaited<T> = T extends Promise<infer A > ? 
                      A extends Promise<infer B > ? 
                        B extends Promise<infer C > ?  
                          C extends Promise<infer D > ? D : C : B: A : 
                            T extends {then: (onfulfilled: (arg: number) => any) => any} ?  number : T 

agb avatar Apr 12 '23 06:04 agb

Hi @agb , I dislike the any type as well but your solution fails in at least 2 cases.

// @ts-expect-error
type error = MyAwaited<number>

// Fails to unwrap more than 4 levels of nested promises
type Z2 = Promise<Promise<Promise<Promise<Promise<string | boolean>>>>>
Expect<Equal<MyAwaited<Z2>, string | boolean>>

To be fair, the last @ts-expect-error test may be considered invalid since the built-in Awaited<T> type fails that test as well.

// Fails
// @ts-expect-error
type error = Awaited<number>

AndrewLamWARC avatar Apr 14 '23 06:04 AndrewLamWARC