tc39 / proposal-promise-with-resolvers

Home Page:http://tc39.es/proposal-promise-with-resolvers/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Array destructuring instead of Object destructuring with rename

coderaiser opened this issue · comments

When you want to create a couple promises you need to write a lot of boilerplate using destructuring with rename:

const {promise, resolve, reject} = Promise.withResolvers();
const {
    promise: readFile,       
    resolve: resolveReadFile,
    reject: rejectReadFile
} = Promise.withResolvers();

Would be great to have ability to use array destructuring instead:

const [promise, resolve, reject] = Promise.withResolvers();
const [readFile, resolveReadFile, rejectReadFile] = Promise.withResolvers();

In a similar to Object.entries() and Array.prototype.entries().

Where code looks like this:

for (const [index, promise] of promises.entries())
    log(i, promise);

It’s much shorter and simpler in a long running distance.

regardless of any merits here, this API change will almost certainly not happen, as the existing spec has reached stage 3, which means at this point, any potential changes must be "only those deemed critical based on implementation experience"

(To be clear, the stage 3 is still conditional, but I expect it to achieve its condition without changes)

I agree with array deconstructing. This is a serious DX problem, and that's also why React hooks return an array instead of an object.

I hope we can discuss this on the committee.

Since it’s already stage 3 and consensus that this is a problem (serious or otherwise) is unlikely, i think it wouldn’t be productive to have that discussion, but certainly you can add it to the agenda if you like.

I'm a bit late to the party; but why not both? I've been rolling a similar construct using array destructuring for years...

Rather than changing the proposal, perhaps we can amend it [in the future] by making the result of Promise.resolve iterable? That is, we can add to the result

{
  promise,
  resolve,
  reject
}

an iterator yielding promise, resolve, and reject

{
  promise,
  resolve,
  reject,
  *[Symbol.iterator](){
     yield this.promise;
     yield this.resolve;
     yield this.reject;
  }
}

giving us the best of both worlds.

const {promise, resolve, reject} = Promise.withResolvers();
// OR 
const [readFile, resolveReadFile, rejectReadFile] = Promise.withResolvers();

When you want to create a couple promises […]

… then you usually do that in separate functions, one per promise, anyway. Do you really think this is a common issue? Can you show examples of real-world code doing this?

When you want to create a couple promises […]

… then you usually do that in separate functions, one per promise, anyway. Do you really think this is a common issue? Can you show examples of real-world code doing this?

import { InvertedPromise } from "@utils/async";
import {OPEN_AI, ANTHROPIC, LOCAL} from "@settings/providers";

export const send = (body) =>{
  const [openAI, resolveOpenAI] = InvertedPromise();
  const [anthropic, resolveAnthropic] = InvertedPromise();
  const [local, resolveLocal]  = InvertedPromise();
  fetch(OPEN_AI.URL, {headers:OPEN_AI.headers}).then(resolveOpenAI);
  fetch(ANTHROPIC.URL, {headers:ANTHROPIC.headers}).then(resolveAnthropic);
  fetch(LOCAL.URL, {headers:LOCAL.headers}).then(resolveLocal);
  return  Promise.race([openAI, anthropic, local]);
}

But why would you write all that as opposed to:

import {OPEN_AI, ANTHROPIC, LOCAL} from "@settings/providers";

export const send = async (body) => {
  return Promise.race([
    fetch(OPEN_AI.URL, {headers:OPEN_AI.headers}),
    fetch(ANTHROPIC.URL, {headers:ANTHROPIC.headers}),
    fetch(LOCAL.URL, {headers:LOCAL.headers}),
  ]);
}

That is really bad code, causing unhandled promise rejections and crashing your application when any of the fetch requests fail. And it doesn't really motivate the use of InvertedPromise or Promise.withResolvers. It should be written just

import {OPEN_AI, ANTHROPIC, LOCAL} from "@settings/providers";

export function send(_body) {
  const openAI = fetch(OPEN_AI.URL, {headers:OPEN_AI.headers});
  const anthropic = fetch(ANTHROPIC.URL, {headers:ANTHROPIC.headers});
  const local = fetch(LOCAL.URL, {headers:LOCAL.headers});
  return Promise.any([openAI, anthropic, local]);
}

It's a trivial example where the alternative boiler-plate would be distinctly "less pretty".
Analyzing the code beyond that might be moving the goal post.

@johnhenry for a use case to be compelling, it has to be a reasonable thing for someone to write. Either way, I find your example exceedingly "less pretty" than the one I provided, although surely that's subjective. I'd love to see a use case that motivates this suggestion where the code couldn't be easily written in a cleaner, more idiomatic, and equally or more correct way. Do you have one?

Apologies. This seems an odd ask as this is an alternative implementation of a non-existent feature for which the use-case is identical.

I'm not arguing that either API is better; but it seems like this interface was shot down, not because of its lack of merit -- but due to the fact that the proposal had passed the stage 3 milestone.

The point here is to demonstrate that we can easily have both interfaces.

I agree with your point.

However, I think that without a compelling use case, then it does indeed lack merit.

Would you consider the use case presented at the beginning of this discussion to be compelling?

No, per #16 (comment), not without a compelling use case to need to create multiple promises with their resolvers all in the same scope.