Skip to content

Infer non returning function to be returning undefined, not void #36239

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

Open
4 of 5 tasks
sharno opened this issue Jan 16, 2020 · 14 comments
Open
4 of 5 tasks

Infer non returning function to be returning undefined, not void #36239

sharno opened this issue Jan 16, 2020 · 14 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@sharno
Copy link

sharno commented Jan 16, 2020

Search Terms

non returning, undefined instead of void, undefined or unknown instead of void

Suggestion

  • Change return of non returning functions to undefined instead of void
  • Accept unknown as a type of non returning functions
    examples:
const f = () => {} // this gets inferred as () => void

const f1 = (): undefined => {} // doesn't work
// error: A function whose declared type is neither 'void' nor 'any' must return a value.(2355)

const f2 = (): unknown => {} // same error

https://www.typescriptlang.org/play/#code/MYewdgzgLgBAZjAvDAFASiQPhgbwL4CwAUMaJLHAIxKpoBcMArmACYCmcAlmGy1rjEIkiZaPABMNdA2YBrMCADuYfvmJA

Motivations

void is an annoying type to deal with, has many inconsistencies and there's a ton of issues around it for its weird behavior like: #35850 , #35236 , #33420 ... etc
Non-returning functions actually return undefined which is a more predictable type to deal with and narrow disjunctions with and even use optional chaining with.

Use Cases

Many use cases appear while using promises where there's a need to catch an error but at the same time not deal with void

const f = () => {}
async (p: Promise<A>) => {
  const a = await p.catch(f);
  // do stuff with a whose type is now A | void
};

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code

Not sure if this might break some code or not but I think any code that dealt with void should work while dealing with undefined?

  • 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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@nmain100
Copy link

I like the current behavior. If I write a function with no return <expr> in it at any point, then there's no reason for me to try to observe the return value of it, and any attempt to observe the return value is probably a mistake. I like it when Typescript catches my mistakes.

@sharno
Copy link
Author

sharno commented Jan 17, 2020

undefined is a more specialized and narrower type that void
My guess is if a function always returns undefined there's no reason to observe its return type either.

Another case is if you are using a function of type () => void in the catch part of a promise, you'll end up with a type like this: Promise<A | void>, at this point you care to know that your catch isn't actually returning A which will be hard to handle

@falsandtru
Copy link
Contributor

I hate the following error. The compiler requires a return statement although I just intend to reject null.

declare function f<T>(g: () => T): void;
f<undefined>(() => { }); // error

@RyanCavanaugh
Copy link
Member

Let's say today you write a function and there's no useful value to return.

Then, next month, you decide that it'd be better if the function returned a number of some kind.

Is this a breaking change in your API contract?

If the non-returning version of the function was marked as void, then no, it can't be a breaking change, because the rules around void effectively prevent a consumer from depending on the return value.

If the non-returning version of the function was marked as undefined, then yes, it absolutely can be a breaking change. Code like this

const p = fn() || 2;

would have been an error under void, but is OK under : undefined and now changes behavior. There is substantial risk of downstream code changing behavior when a function goes from returning "nothing" to returning "something"; having the "nothing" case be void mitigates that risk.

What are we getting in return? The "confusion" around void is generally around people not understanding what it means to mark a function's return value as unspecified, which is its primary source of future-safety.

@sharno
Copy link
Author

sharno commented Jan 17, 2020

I disagree, for me, specified types/behavior trumps unspecified types/behavior. and I think that contradicts with your comments before that specified behavior should be annotated as so. Non-returning functions have a specified behavior too.
#35236 (comment)

Oh, and that console.error and its friends should have their return types changed from void to undefined since that behavior is specified

Anyway, if you plan not to accept this issue, is it possible to accept an new issue to allow this?

function a(): undefined { }
function b(): unknown { }

@RyanCavanaugh
Copy link
Member

I think those are pretty reasonable, at least with noImplicitReturns off

@falsandtru
Copy link
Contributor

@RyanCavanaugh #36239 (comment) is not the issue here. I mean the compiler can know that the return type can be undefined from the function body. This is an issue of type inferences rather than type checking.

@falsandtru
Copy link
Contributor

If the non-returning version of the function was marked as undefined by the type of the receiver, probably it can't be a breaking change.

@falsandtru
Copy link
Contributor

falsandtru commented Jan 18, 2020

So I disagree with author's use case but the compiler should allow non-returning functions to return the undefined type in some cases.

@falsandtru
Copy link
Contributor

I separated the issues into #36288 because declaring undefined return type with no context is not my main intent here.

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Feb 6, 2020
@ssalka
Copy link

ssalka commented Oct 12, 2020

EDIT: it looks like #36288 might be a better place for me to raise the below case? Not really sure, as both issues seem relevant.


I'm running into a situation where I have a func(): Promise<void | Result> which can possibly return a value, but not necessarily so.

At the call site, I'm trying to do:

const res = await func();

if (res?.field) {
   ...
}

however the compiler flags this an error, as optional chaining doesn't work on void types (assuming this is intended behavior?). Oddly, it does work if I instead do:

const res = await func();

if (res && res.field) {
   ...
}

This seems weird to me, since I expect res?.field to be equivalent to / interchangeable with res && res.field.

The only thing I can think to do, besides using the res && res.field condition, is changing the function to instead return Promise<undefined | Result>, however I dislike this approach because it requires that the function explicitly return undefined, which I consider to be unnecessary.

@AlexGalays
Copy link

AlexGalays commented Nov 4, 2020

This would be very useful.

Imagine a function a bit like found in immer where the return type is void, and an object is passed for the sole purpose of being mutated (it's actually a proxy that doesn't get mutated but that's how the api looks like from the outside)

so you expect people to write something like:

update(obj, draft => {
  draft.name = 'the new name'
})

If one doesn't pay attention, it's very easy to return a transformed object and expect this change to be picked up. It won't do anything and there is no compilation error:

update(obj, draft => newObjFromApi) // Oh no, this won't do anything.

This is not a fiction by the way, this happened 5 minutes ago 😆
The type system could really help us detect these usage mistakes.

EDIT: Nevermind, it seems () => void | undefined already does exactly that. Is there any drawback?

@ChocolateLoverRaj
Copy link

void is an annoying type to deal with

@sharno I agree

I think the void type should be removed from typescript.

@wojpawlik
Copy link

wojpawlik commented Feb 10, 2021

If the non-returning version of the function was marked as undefined, then yes, it absolutely can be a breaking change. Code like this

const p = fn() || 2;

would have been an error under void, but is OK under : undefined and now changes behavior.

@RyanCavanaugh, perhaps TypeScript should complain about this code? typescript-eslint already does:

Unnecessary conditional, value is always falsy                                                             @typescript-eslint/no-unnecessary-condition
Unexpected nullish value in conditional. The condition is always false                                     @typescript-eslint/strict-boolean-expressions
Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator  @typescript-eslint/prefer-nullish-coalescing

undefined is a pretty useless type already, I only found one more way to misuse it:

function stop(reason = 'unspecified') {}
function fn() {
    return undefined
}

stop(fn())

It's possible to similarly misuse void:

function fn() {}

new Promise<void>((resolve) => resolve(fn()))
// or
function bar(): void {
    return fn()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants