Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.0: Cannot use variadic tuples in Promise.all #39788

Closed
tom-sherman opened this issue Jul 28, 2020 · 12 comments
Closed

4.0: Cannot use variadic tuples in Promise.all #39788

tom-sherman opened this issue Jul 28, 2020 · 12 comments
Assignees
Labels
Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@tom-sherman
Copy link

TypeScript Version: 4.0.0-beta

Search Terms:

variadic tuples promise.all

Code

declare const foo: Promise<string>;
declare const bar: Promise<number>[];

async function m() {
    const p = await Promise.all([
        foo,
        ...bar
    ])
}

Expected behavior:

p should have type [string, ...number]

Actual behavior:

Type error

Playground Link: https://www.typescriptlang.org/play/?ts=4.0.0-dev.20200728&ssl=9&ssc=2&pln=1&pc=1#code/CYUwxgNghgTiAEYD2A7AzgF3gMyUgXPAAoxIC2AlmiADyYwUoDmAfANwCwAUKJLAsnRYARrEIlyVWigCuZYSBgsA2gF1OXblDQBPFGBwz9GCqnhkAFAEp4Ab27xHiVJngAHeAF54UAO5QKLAlKagA6KAgIC2UHJzjcJAAaWLjHUPTRGBT4VStuAF8gA

Related Issues: none

@tom-sherman
Copy link
Author

tom-sherman commented Jul 28, 2020

I'm guessing the type definitions for Promise.all haven't been updated to take advantage of variadic tuples. I'm not sure of the process here, can I submit a PR to update them targeting the 4.0 branch?

Here's some code using a new definition for Promise.all where it works almost as expected:

declare const foo: Promise<string>;
declare const bar: Promise<number>[];

declare interface PromiseConstructor {
    all<T extends readonly unknown[]>(values: readonly [...T]): Promise<[...T]>
}

async function m() {
    const p = await Promise.all([
        foo,
        ...bar
    ])
}

Playground link

For some reason p has a type of [Promise<string>, ...Promise<number>[]] but maybe I'm doing something wrong.

EDIT: Followup, I got it working!

declare const foo: Promise<string>;
declare const bar: Promise<number>[];

declare interface PromiseConstructor {
    all<T extends readonly unknown[]>(values: [...T]): { [P in keyof T]: T[P] extends Promise<infer U> ? U : never }
}

async function m() {
    const p = await Promise.all([
        foo,
        ...bar
    ])
}

Playground link

@ahejlsberg
Copy link
Member

Here's a working definition:

type Awaited<T> = T extends PromiseLike<infer U> ? U : T;

declare function all<T extends unknown[]>(values: readonly [...T]): Promise<{ [P in keyof T]: Awaited<T[P]> }>;

We have yet to update the Promise.all definition to use variadic tuples. It's not clear that we can without breaking some code, specifically in cases where type arguments are explicitly specified:

let p = Promise.all<string, number>([p1, p2]);

With the variadic tuple definition that would have to be written

let p = Promise.all<[string, number]>([p1, p2]);

@rbuckton Anything to add?

@tom-sherman
Copy link
Author

tom-sherman commented Jul 28, 2020

@ahejlsberg

Here's a working definition:

Ha, you must have commented just as I figured out the definition on my own - good timing!

Would be a shame to not be able to make this change as it seems like a perfect use case for variadic tuples in the standard library.

I wonder if the variadic tuple form could be added as another overload?

@rbuckton
Copy link
Member

I've had concerns about Awaited<T> in general because it doesn't match the ECMAScript behavior of await or the "recursive unwrap" nature of native Promises, since Awaited<T> can't be defined recursively. Also, I tried something like this in #39336 and ran into issues: /~https://github.com/typescript-bot/TypeScript/pull/54/files#diff-0e62d568d28b82cee9e66b2b8fafd290R7.

The issue we run into is that if we move the variadic tuple version first in the overload list (so that we use it when performing inference for the type arguments), then we run into issues with existing Promise.all<[string, number]>([p1, p2]) when p1 is a Promise<[string, number]>, especially when that type comes from further inference. If we move it last, then we don't end up using it at all. The problem is that a tuple here can have two meanings:

  1. The type of each promise in the array (i.e. [Promise.resolve("a"), Promise.resolve(1)])
  2. The type of all promises in the array (i.e. [Promise.resolve(["a", 1]), Promise.resolve(["b", 2])])

I'm not sure we can properly fix one without breaking the other, unless we can modify the inference and overload resolution algorithms in a non-breaking way.

@ahejlsberg
Copy link
Member

@rbuckton Regarding Awaited<T>, I think it is fine to just definite it with a manually unrolled list of unwrappings up to some maximum level. For example:

type Awaited<T>  = T extends PromiseLike<infer U> ? Awaited4<U> : T;
type Awaited4<T> = T extends PromiseLike<infer U> ? Awaited3<U> : T;
type Awaited3<T> = T extends PromiseLike<infer U> ? Awaited2<U> : T;
type Awaited2<T> = T extends PromiseLike<infer U> ? Awaited1<U> : T;
type Awaited1<T> = T extends PromiseLike<infer U> ? U : T;

It's not exactly clear to me what would break by putting an overload with the definition above (which is different from the one you had in your PR) first in the list. I think it would just permit Promise.all<[string, number]>(...) to either take a tuple or an array of tuples. But maybe I'm missing something.

@rbuckton
Copy link
Member

There were a number of things awaited T was intending to solve that Awaited<T> couldn't, such as recursive unwrap, handling malformed self-recursive and mutually recursive promise types, handling non-promise thenables, etc. Also, the conditional type logic doesn't work correctly when not in --strictNullChecks mode for cases like Awaited<undefined> (at least, it didn't back in 3.7 when we were looking at some other community PR's intending to introduce Awaited<T>). I'm still investigating awaited T as a possibility for a future version.

@tom-sherman
Copy link
Author

It's worth pointing out that the declarations in @ahejlsberg's comment have the generic params inferred as T[P][] and not Awaited<T[P]>[] eg.

type Awaited<T> = T extends PromiseLike<infer U> ? U : T;

declare function all<T extends unknown[]>(values: readonly [...T]): Promise<{ [P in keyof T]: Awaited<T[P]> }>;

declare const p1: Promise<number>[];

declare const p2: Promise<string>;

all([p2, ...p1]) // function all<[Promise<string>, ...Promise<number>[]]>

@millsp
Copy link
Contributor

millsp commented Aug 13, 2020

@rbuckton what is the downside of doing a Promise unwrap this way?

type Promise<A> =
    A extends globalThis.Promise<any>
    ? A
    : globalThis.Promise<A>

type test = Promise<Promise<Promise<"typescript">>> // Promise<"typescript">

@spideythewebhead
Copy link

Hello, i have one question

wait<T extends Promise<any>[]>(...promises: T): Promise< array of T array types>

can we somehow do that? example

wait(Promise.resolve(0), Promise.resolve('test')) => result type = [number, string]

because for now i got this


type PromiseResult<T> = {
  status: 'resolved' | 'rejected'
  value?: T
  error?: any
}

type PromiseResultMap<T> = {
  [P in keyof T]: T[P] extends Promise<infer U>
    ? PromiseResult<U>
    : PromiseResult<never>
}

wait<T extends Promise<any>[]>(...promises: T): Promise<PromiseResultMap<T>>


The PromiseResultMap in vscode intellisense resolves as an array but the actual type is an Object with keys the indexes of the array, but i can not do something like results.length or destruct it [result1, result2] because of results is not iterable

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Aug 18, 2020
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.1.0 milestone Aug 18, 2020
@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label Dec 11, 2020
@elumixor
Copy link

@spideythewebhead, something like this?

function all<T extends any[]>(...values: { [K in keyof T]: PromiseLike<T[K]> }) {
    return Promise.all(values) as Promise<T>;
}

@privatenumber
Copy link

privatenumber commented May 29, 2021

FYI variadic tuple support within Promise.all is being experimented here #39796

@RyanCavanaugh
Copy link
Member

This works now!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
9 participants