-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Bug: await-thenable doesn't handle unions cleanly in for-await [8.8.0 regression] #10080
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
Comments
Hi @JavaScriptBach! Could you expand a bit on what a a concrete use case looks like when this comes up, and why it would make sense in general to use Btw for other readers, here is the sample code const asyncIter: AsyncIterable<string> | Iterable<string> = [];
for await (const s of asyncIter) { }
export { } |
Note also that there is a suggestion, but no autofix, since the fix would not be safe in general. |
Ah sorry you're right, it is a suggestion, not an autofix. Closing; it looks like everything here is working as intended. My use case is that I have some library code that intentionally takes in a parameter typed |
Well, the check for async iterables was just added this morning 🙂 #10008... so it may be "working as intended", but we're definitely listening closely at this time for feedback on whether the design is good in the first place or not! I'd definitely invite you to continue the discussion here if you would like to make a case for an improvement to the behavior around |
This rule has a high batting average on my company's codebase, so overall I'm in favor of it. It gives a false positive when operating on union types that may need |
❤️
This would definitely be simple to implement. I'm just looking to understand what the justification to do so is. |
I think that this should be considered a bug personally because the rule does not report on a similar union case for non-iterables: declare const maybePromise: Promise<number> | number;
await maybePromise;
|
That is true, however, |
in other words, it's quite intentional that the rule flags on declare const arrayOfPromises: Array<Promise<number>>;
for await (const x of arrayOfPromises) { // bad
x satisfies number;
} But perhaps being somewhat lax with the union case is a worthwhile devX concession for useful real-world scenarios. That's the angle by which I'm the most likely to be persuaded. |
(see also #8858 (comment)) |
Yeah I would be looking at it from the perspective that if you're attempting to iterate a You could probably fashion some code that allows you to split things into two loops - one that's async and one that's not. But my question would be - why would you do that when that would require you to duplicate code. Its the same reason we don't force people to do this for the regular promise union case: const result = (value instanceof Promise) ? await value : value; Sure this code is "more correct" but it's also not any better or any clearer. So yeah I'd argue that from a DevX perspective flagging this is technically correct, but realistically it's fine to await this specific union case. |
Yeah, I think I was radicalized by a couple of proposals (this and this) I read a few years ago to believe that it was specifically bad to mix async and sync iterators, but that's probably not terribly relevant here. The main difference I see between the So, whereas the difference between the following two lines declare const value: Promise<string> | string;
const version1 = (value instanceof Promise) ? await value : value;
const version2 = await value; is basically only observable via the incurring of an extra microtask tick, the demo code for `for await` vs `for`function resolveAfter(ms: number) {
return new Promise<void>(resolve => { setTimeout(() => resolve(), ms) });
}
function * yield123Sync() {
yield 1; yield 2; yield 3;
}
async function * yield123Async() {
await resolveAfter(100);
yield 1;
await resolveAfter(100);
yield 2;
await resolveAfter(100);
yield 3;
}
function * yieldPromise123Sync() {
yield resolveAfter(100).then(() => 1);
yield resolveAfter(100).then(() => 2);
yield resolveAfter(100).then(() => 3);
}
async function printValuesAsync<T>(iterable: Iterable<T> | AsyncIterable<T>) {
const startTime = performance.now();
console.log('starting to iterate (async flavor)');
for await (const value of iterable) {
console.log(value);
}
const endTime = performance.now();
console.log(`done iterating (async flavor). Completed after ${endTime - startTime} ms`);
}
async function printValuesMixed<T>(iterable: Iterable<T> | AsyncIterable<T>) {
const startTime = performance.now();
console.log('starting to iterate (mixed flavor)');
if (Symbol.asyncIterator in iterable){
for await (const value of iterable) {
console.log(value);
}
} else {
for (const value of iterable) {
console.log(value);
}
}
const endTime = performance.now();
console.log(`done iterating (mixed flavor). Completed after ${endTime - startTime} ms`);
}
async function main() {
await printValuesAsync(yield123Sync()); // ~ 1 ms
await printValuesAsync(yield123Async()); // ~309 ms
await printValuesAsync(yieldPromise123Sync()); // ~311 ms
console.log('compare to');
await printValuesMixed(yield123Sync()); // ~2.7 ms
await printValuesMixed(yield123Async()); // ~311 ms
// the control flow here is quite different.
await printValuesMixed(yieldPromise123Sync()); // ~5 ms
}
main() So, anyway, what I'm really interested to understand, and don't currently understand, is - why would one end up with If it should be iterated synchronously, then I would say the API is better expressed as a non-promise-returning API in the first place, in which case the implementation will not be using If it should be iterated asynchronously/sequentially, then I would say that This is all to say, I'm certainly open to there being lenient handling of unions containing |
My code uses From reading the above comments, what I understand is that the control flows of So, the only difference between |
Hello I just opened a PR to fix this issue, However I don't know if that will resolve it for your example const asyncIter: AsyncIterable<string> | Iterable<string> = [];
for await (const s of asyncIter) { } I think Typescript will automatically deduct that |
Yeah, that's just TS being clever and narowing declare const asyncIter: AsyncIterable<string> | Iterable<string>;
for await (const s of asyncIter) { }
export { } or async function foo(asyncIter: AsyncIterable<string> | Iterable<string>) {
for await (const s of asyncIter) { }
} |
Uh oh!
There was an error while loading. Please reload this page.
Before You File a Bug Report Please Confirm You Have Done The Following...
Playground Link
https://typescript-eslint.io/play/#ts=5.4.3&showAST=es&fileType=.tsx&code=MYewdgzgLgBAhhAnmYBJKBTATgLhgQSRXWzgCMAbDAHmiwEswBzAPhgB8YStyraoGzNgF4YAbQC6AbgBQAMxBZ4Adzj1YAClCRYEGCDnwiaTFgCUMAN4wAvjIwAPAA6LY1m0A&eslintrc=N4KABGBEBOCuA2BTAzpAXGUEKQAIBcBPABxQGNoBLY-AWhXkoDt8B6AQwHd3K78ALRE3YAjJOiiJo0APbRI4MAF8QSoA&tsconfig=N4KABGBEDGD2C2AHAlgGwKYCcDyiAuysAdgM6QBcYoEEkJemy0eAcgK6qoDCAFutAGsylBm3TgwAXxCSgA&tokens=false
Repro Code
ESLint Config
tsconfig
Expected Result
If I call
for await
on an iterator that is typed asAsyncIterable<string> | Iterable<string>
, I expect one of the following to happen:for await
works on both sync and async iterables)await
because that would introduce a code bug.Actual Result
Lint error, with an unsafe autofix suggesting to remove the
await
.Additional Info
No response
The text was updated successfully, but these errors were encountered: