ghaiklor / type-challenges-solutions

Solutions for the collection of TypeScript type challenges with explanations
https://ghaiklor.github.io/type-challenges-solutions/
Creative Commons Attribution 4.0 International
474 stars 57 forks source link

type-challenges-solutions/en/easy-awaited #24

Open utterances-bot opened 3 years ago

utterances-bot commented 3 years ago

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

dvlden commented 3 years ago

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!

ghaiklor commented 3 years ago

@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.

pragmasoft-ua commented 2 years ago

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

ghaiklor commented 2 years ago

@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?

pragmasoft-ua commented 2 years ago

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.

ghaiklor commented 2 years ago

@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?

pragmasoft-ua commented 2 years ago

@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

DaniGuardiola commented 2 years ago

@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.

S-Mann commented 2 years ago

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.

farukEncoded commented 2 years ago
type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer R>
  ? R extends Promise<unknown>
    ? MyAwaited<R>
    : R
  : T;
farukEncoded commented 2 years ago

Or

type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer R>
  ? R extends Promise<unknown>
    ? MyAwaited<R>
    : R
  : never;
AshNaz87 commented 2 years ago

This seems to work:

type MyAwaited<T extends Promise<any>> = T extends Promise<infer R> ? Awaited<R> : never
stasgavrylov commented 2 years ago

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

it doesn't count

misterhomer1992 commented 1 year ago

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;
AndrewLamWARC commented 1 year ago

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
huangzonggui commented 1 year ago
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer R> ? Awaited<R> : T
agb commented 1 year ago

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 
AndrewLamWARC commented 1 year ago

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 type fails that test as well.

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