microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.77k stars 12.46k forks source link

Types for async functions do not handle `void` assignments like normal ones #33420

Open weswigham opened 5 years ago

weswigham commented 5 years ago

TypeScript Version: 3.4.0-dev.201xxxxx

Search Terms:

Code

const w: void = "";

const x: () => void = () => "";

const y: () => Promise<void> = async () => "";

function a(): void {
    return "";
}

async function b(): Promise<void> {
    return "";
}

const y2: () => Promise<() => void> =  async () => () => "";

function a2(): () => void {
    return () => "";
}

function b2(): Promise<() => void> {
    return () => "";
}

Expected behavior: Generally speaking, all of them should be an error, or all of them shouldn't be.

Actual behavior: We have a special "function returning void" rule that allows () => "" to be assignable to () => void, but this doesn't carry thru to async scenarios, so a async () => "" is not assignable to a () => Promise<void>.

Playground Link

weswigham commented 5 years ago

There are two rules we have in place governing the above:

  1. void-returning functions should not return non-undefined values.
  2. A function type whose return value isvoidis treated as though thatvoidwereunknown`, for the purposes of assignability.

This creates this annoying inconsistency where even though function statement/expressions with annotated returns of void are checked for undefined-ness, consts with function type annotations to which function expressions are assigned are not checked in the same way. Separately, the second rule fails to affect async functions in situations where you'd expect it should, since they don't return void verbatim, but instead return Promise<void>.

fatcerberus commented 5 years ago

This has caused me pain already when writing typings for miniSphere. Generally speaking I like the rule where a void return acts as any for the purpose of assignment, as it’s useful in HOF scenarios (callbacks e.g.), and it frustrates me that I can’t get the same behavior with async functions.

fatcerberus commented 5 years ago

This does seem a bit tricky though: if we made the void assignment rule work for Promise<void> would it then end up affecting any generic with a void type argument? I’m not sure whether that’s desirable...

And then even if it is... what about when the type parameter is contravariant? Or invariant? Does variance affect it at all? It’s a pretty big can of worms.

weswigham commented 5 years ago

Tbh, I think void just needs to behave like unknown except we also have lint-level checks to attempt to forbid real-valued returns in implementations whose expected return position type is void.

fatcerberus commented 5 years ago

I don’t know, I think it’s kind of elegant right now that void is in a duality with any. :smiley:

Where “you” = programmer, “I” = type system.

Dan503 commented 2 years ago

I've noticed that void doesn't behave as expected for the following circumstance either:

This should throw an error because () => () => void != () => void

type BasicFn = () => void
// Should throw an error but doesn't
const x: BasicFn  = () => () => console.log('hello')

This bug caused me to spend a multiple hours trying to figure out what I was doing wrong.

It's not easy spotting that () => () => {} should be () => {} especially when you are expecting TS to detect type conflict issues like this.