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

Revisit void/unknown/undefined interactions #52486

Closed
5 tasks done
Diggsey opened this issue Jan 29, 2023 · 8 comments
Closed
5 tasks done

Revisit void/unknown/undefined interactions #52486

Diggsey opened this issue Jan 29, 2023 · 8 comments
Labels
Experience Enhancement Noncontroversial enhancements Suggestion An idea for TypeScript
Milestone

Comments

@Diggsey
Copy link

Diggsey commented Jan 29, 2023

Current situation

void is a special type which behaves like unknown with three exceptions (that I am aware of):

  • When used as a function return type, the function must return undefined.
  • The compiler assumes that void === undefined for the purposes of narrowing. In other words, this type-checks where unknown would not:
    function bar(x: string) {}
    
    function foo(x: void | string): void {
        if (x !== undefined) {
            bar(x)
        }
    }
  • Functions which return void may have non-returning code branches.

Problems

Firstly, I am aware that the unsoundness caused by these special cases is intentional: that in itself is not the issue I am raising. The issue is that with other unsoundness, you can usually opt-in to more sound type-checking. For example, with any, users have the option to not use it - instead using unknown or generics to get a more sound (if more verbose) solution.

With void, we have no choice. There is no way to say that a function returns undefined, because the following does not type-check:

function foo(): undefined {} // A function whose declared type is neither 'void' nor 'any' must return a value.(2355)

This stems from the fact that void servese two purposes:

  1. To relax type-checking
  2. To annotate that a function intentionally does not explicitly return a value

There are additional problems with void when it comes to async functions. Since Promise<void> is not compatible with Promise<undefined> it works against itself, resulting in spurious type errors where there should be none.

Suggestion

  1. Allow implicit returns from any function definition whose return-type is compatible with undefined.
  2. When unspecified, infer undefined instead of void for functions which do not return a value.
  3. [Optional] Change the Not all code paths return a value. warning to fire only when some but not all codepaths have an explicit return.

Of these (1) is not a breaking change. (2) may cause some breakage if the return-type of such a function is extracted via ReturnType<F>. (3) would cause warnings for functions which use a mix of implicit/explicit returns.

For this reason, it may be desirable to implement these changes behind a new configuration option.

Overall this would improve typescript because it allows more valid code to compile, while providing more sound type-checking. Common errors like accidentally ommitting a return statement will still be caught. The language will be simpler to learn because void and its special rules will be encountered less often. These changes could be the start of a gradual phase-out of the void type entirely in favour of unknown.

🔍 Search Terms

void undefined unknown explicit implicit return function unsound soundness

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@MartinJohns
Copy link
Contributor

Related: #42709

@RyanCavanaugh
Copy link
Member

I think this is a duplicate of #42709 (if it's not, a compare/contrast is necessary)

Re this error:

function foo(): undefined {}

I agree it's silly. I think we would take a PR that allows no return statements in a function returning exactly undefined, even if noImplicitReturns is set. I'll have to run it by folks, but I'll co-opt this issue to track that rather than mark as duplicate.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Experience Enhancement Noncontroversial enhancements labels Jan 30, 2023
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jan 30, 2023
@RyanCavanaugh
Copy link
Member

Updates from the team chat:

  • No one likes implicit returns, I guess? 😅
  • But if you're going to implicitly return, implicitly returning undefined is clearly the best way to do it
  • 👍

@MartinJohns
Copy link
Contributor

I think we would take a PR that allows no return statements in a function returning exactly undefined, even if noImplicitReturns is set.

Allowing implicit returns when implicit returns are explicitly disallowed feels really weird.

@Diggsey
Copy link
Author

Diggsey commented Jan 30, 2023

@MartinJohns with noImplicitReturns disabled you get no error at all even if the function is supposed to return eg. a number. That's behaviour that we'd really want to continue to detect. Although perhaps that option is poorly named.

@RyanCavanaugh
Copy link
Member

noImplicitReturns is indeed a bit poorly named. If the setting never let you exit a function through the end of the statement block, that'd be a consistent rule (albeit one you could just lint). The current rule says you can exit that way as long as the return type is void or any, and not allowing undefined in those carve-outs feels like a miss rather than an intentional behavior.

It's all pretty ironic since, for all the holes our type system has, knowing that a function with no return statements will return undefined is a 100% guarantee!

@fatcerberus
Copy link

I always figured it was intentional: if you annotate as void, that suggests the caller isn’t meant to use the return value at all and therefore an explicit return is redundant. If you annotate as undefined then that’s an explicit contract so the return should be explicit as well.

@whzx5byb
Copy link

Fixed by #53092

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experience Enhancement Noncontroversial enhancements Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants