Skip to content

Inconsistent missing await in conditional errors, invalid quick fix #43514

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

Closed
bagdonas opened this issue Apr 3, 2021 · 4 comments · Fixed by #43593
Closed

Inconsistent missing await in conditional errors, invalid quick fix #43514

bagdonas opened this issue Apr 3, 2021 · 4 comments · Fixed by #43593
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@bagdonas
Copy link

bagdonas commented Apr 3, 2021

🔎 Search Terms

  • missing await in conditional
  • await
  • conditional

🕗 Version & Regression Information

Since 4.3.0-dev

⏯ Playground Link

Playground link with relevant code

💻 Code

declare const cache: Record<string, Promise<string>>

declare const fetchThing: (id: string) => Promise<string>

const getCachedThing1 = (id: string) => {
  let thing = cache[id]

  return !thing // not an error, if the error in getCachedThing3 is valid then why this isn't an error?
    ? (cache[id] = fetchThing(id))
    : thing
}

const getCachedThing2 = (id: string) => {
  let thing = cache[id]

  return thing ?? (cache[id] = fetchThing(id)) // not an error
}

const getCachedThing3 = (id: string) => {
  let thing = cache[id]

  return thing // error (2801), but it should't be, getCachedThing3 isn't async function
    ? thing
    : (cache[id] = fetchThing(id))
}

After applying quick fix to getChachedThing3 error:

const getCachedThing3 = (id: string) => {
  let thing = cache[id]

  return await thing // error (2801), but it should't be, getCachedThing3 isn't async function
 // error (2801), but it should't be, getCachedThing3 isn't async function
    ? thing
    : (cache[id] = fetchThing(id))
}

Quick fixed code is invalid (await is added to non-async function). Also, comment gets duplicated.

🙁 Actual behavior

  1. Missing await in conditional errors are inconsistent
  2. Missing await in conditional errors get reported in non-async functions
  3. Quick fixing the error in non-async function produces invalid results

🙂 Expected behavior

  1. Missing await in conditional errors to be consistent
  2. Missing await in conditional errors to NOT be reported in non-async functions
  3. No quick fix suggestion in non-async function

As a sidenote, I think the "missing await in conditional error" feature should be under a flag

  1. It is wildly inconsistent
  2. This is a job for linters
  3. Objects are thruthy, so let them be thruthy :)
  4. This feature breaks existing code
@andrewbranch
Copy link
Member

andrewbranch commented Apr 5, 2021

This example is unfortunate because the type of thing comes from reading from an index signature, which by default we treat as always-defined but in reality must usually be checked for undefined. The error goes away and everything works as expected if you use --noUncheckedIndexedAccess, or simply write the type annotation for thing:

let thing: Promise<string> | undefined = cache[id];

EDIT: As pointed out in #43565, I screwed this example up ☝️. What I meant to write was

let thing = cache[id] as Promise<string> | undefined;

getCachedThing1 is not an error because we just don’t consider nested expressions for this missing await check. In that function, we don’t see a Promise being tested for truthiness, we see a boolean being tested for truthiness, which is always fine.

getCachedThing2 is not an error because we don’t do definitely-truthy checks on operands of logical binary expressions. We generally want to allow you to code more defensively than your types suggest you need to.

getCachedThing3 matches the pattern of something we think looks suspicious, again primarily because the type of thing should include undefined but doesn’t. I think we should be doing the same thing here as we do for uncalled function checks, which is to silence the error if the expression being checked for truthiness is accessed in the true side of the conditional body. I don’t see that discussed at #39175, but it would make sense to me for those to work the same.

The fact that the await is added to a non-async function was an intentional decision, because the error on the added await gives you access to a subsequent codefix to make the function async. We were concerned about cases where you're editing a large function and the async keyword would get added way off-screen, so we wanted to make that choice more obvious to the user. But that's a decision we could revisit.

The duplicated comment is a bug.

@bagdonas
Copy link
Author

bagdonas commented Apr 6, 2021

Thank you for very detailed explanation. The reason I found these behaviours "sketchy" to the point of filling out an issue is because from the perspective of someone who doesn't know these very specific details (and a sample of 1 such someone) the feature seems broken. I was expecting it to be more generic. Now that I went back and read 4.3 beta announcement again, everything checks out. It works just like advertised.
It still seems super weird that the error is triggered by one specific pattern leaving all other patterns that produce conceptually identical results unchecked. It also feels weird that typechecker is checking for such a pattern (feels like it disregards truthiness of objects). Lots of feelings, I know... 😄

@andrewbranch
Copy link
Member

feels like it disregards truthiness of objects

How do you mean? The general idea is that doing if (somePromise) looks like a mistake precisely because we believe somePromise is an object and objects are always truthy, and therefore should never need to be checked for truthiness. And we issue the error on Promises specifically because there’s a plausible assumption that what you meant to do was await it. We don’t give the same error for random objects like if (someNonPromiseObject) because even though that also looks to be always truthy, we have no guesses about what you meant to do instead, so we just let you do it, assuming you may have a good reason we don’t know about.

@bagdonas
Copy link
Author

bagdonas commented Apr 6, 2021

feels like it disregards truthiness of objects

Promise is just an object, special handling for something that is truthy in a ternary/if feels weird. Especially when use of such value in a condition operand results in typechecker error.

We don’t give the same error for random objects like if (someNonPromiseObject) because even though that also looks to be always truthy, we have no guesses about what you meant to do instead, so we just let you do it, assuming you may have a good reason we don’t know about.

As twisted as my logic is, exactly for this reason. I'm so accustomed to never getting errors when checking values for their truthiness that getting one with this new feature feels weird. Again, just a feeling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants