-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
#AvoidTheVoid Allow users to avoid the void
type.
#42709
Comments
We've spent several dozen person-hours in the past few months beating this particular horse to death without much forward progress, but I remain optimistic that we'll eventually make some progress on it. |
This does sound like it's in the same spirit of #41016. Maybe we should type |
Coming in with the most embarrassing counterpoint ever: I like 'void'. I like typing it, that is, I enjoy the experience clacking V-O-I-D on my keyboard. I do not like clacking U-N-K-N-O-W-N on my keyboard. Not knowing things makes me feel like I don't understand my own code, which makes me feel like a bad programmer. Clacking A-N-Y on my keyboard makes me feel like my keyboard should be taken away and given to a responsible adult. More seriously, I do feel your pain with regard to how Initially, it may have seemed like the difference would be obvious from context:
It must have been judged as a reasonable inference that functions that look like But there are simple cases where this assumption already leads the type checker astray:
Here the type of I'm reviewing your suggestions to see if I actually disagree with any of them. I think the answer is: I don't. I'm surprised by that given that my feeling about the keyword is much more positive than yours. Overall, the two definitions of I'll mention a case where I do like
However, because this is a case where Also, in the interest of fully airing out my laundry: you mention The only problem with that at present is that From my playing around, it looks like the situation 'on the ground' is: when Needless to say, this arrangement pleases no-one. But if the prickly second definition of |
JS already has too many "null" concepts and then TypeScript adds more. Especially when |
void
type.
I second what @MattiasMartens said that using I want to further suggest that the usage of When declaring a function with explicit return type to enforce the intent. function foo(): void {} This avoid the implementation later changed to return something by accident. As for using For callbacks, declaring: const callback: () => void
// or
function fn(handler: (a: A) => void) { ... } Is really a "suggestion" that "hey, I don't need the return value of the callback/handler. As long as it's a function that takes a parameter But when such "suggestion" is being enforced by the compiler, all sort of issues surface. That's why |
The reason you want to use function doSomething(callback: () => void) {
// Meant to do something else, idk what
// Errors if using void, otherwise does not
if (callback()) {
}
} |
It makes sense that that is the case, although I must say that in all my years of TS I've never seen that error:
|
In the "infer functions with implicit returns and no return type as returning undefined" section is this example: declare let p: Promise<number>;
// inferred as Promise<number | void> while Promise<number | undefined> would be SO MUCH NICER here.
const q = p.catch((err) => console.log(err)); However that I think you meant this: declare let p: Promise<number>;
// inferred as Promise<number | void> while Promise<number | undefined> would be SO MUCH NICER here.
const q = p.catch((err) => { console.log(err) }); |
Suggestion
Allow developers to avoid the
void
type.I’d like to propose a couple changes to TypeScript to make
void
an optional type, and inferundefined
in more places wherevoid
is currently used. I’ve argued in multiple places that TypeScript’svoid
type is confusing and detrimental to development, and I was asked by a maintainer to create the following document to outline the problems withvoid
and propose potential solutions.The Problem
First, what is
void
? According to the TypeScript handbook:In practice, the
void
type is more or less a black hole kind of type. Nothing is assignable tovoid
except itself,undefined
andany
, andvoid
is not assignable to anything except itself,unknown
andany
. Additionally, typical type narrowings liketypeof x === "string"
do not narrowvoid
, andvoid
infects type unions likevoid | string
by making the resulting type behave likevoid
.The handbook hints at the original use-case for void, which is that it was used to model functions which don’t return, i.e. functions without a
return
statement. Although the runtime behavior of these functions is to returnundefined
, maintainers argued for modeling these functions as returningvoid
instead at the type level because implicit return values are seldom used, and if they are used this usually indicates some kind of programmer error. The reasoning was that if you actually wanted a function to have a return type ofundefined
, you could do so by explicitly returningundefined
from the function.This section of the TypeScript handbook is a good start, but it doesn’t fully address the intricacies of the
void
type. Whilevoid
behaves mostly like a special type alias forundefined
when used in typical assignments, it behaves more likeunknown
when used as the return type of a function type (() => void
).Again, this makes sense according to the original motivations for void: if you’re marking a callback’s return type as void, then you don’t really care what the callback returns, because you’ve essentially promised that you’ll never use it. It’s important to note that
void
came very early in TypeScript’s development, and neither theunknown
type nor even strict null checks existed at its point of creation.Unfortunately, this dual nature of
void
as being eitherundefined
orunknown
based on its position has an undesirable consequence, which is that whileundefined
is always assignable tovoid
,void
is not assignable toundefined
. Personally, I noticed this issue with increasing frequency because of my work with async and generator functions, to the point where I felt that thevoid
type was getting in my way. These function syntaxes also did not exist when thevoid
type was created, and their very existence violates the key assumption which motivated the creation of thevoid
type in the first place, that we don’t or shouldn’t use the return values of functions with implicit returns.This assumption collapses when we consider async and generator functions because we more or less must use the return values of async and generator functions even if they return implicitly. Many async functions have implicit returns, where the function body is used to await other promises, and the vast majority of generator functions do not contain return statements at all. Despite this, it would be a basic programmer error not to use the returned promises or generator objects in some way, so much so that there are even lint rules like
no-floating-promise
to prevent you from ignoring the return values of these types of functions.Because we must use the return values of async and generator functions, and because they are inferred to return compositions of
void
likePromise<void>
orGenerator<any, void>
,void
types inevitably leak into assignments. I have frequently been frustrated by attempts to assignPromise<void>
toPromise<undefined>
, orGenerator<any, void>
toGenerator<any, undefined>
; invariably, the error pyramids which TypeScript emits all end withvoid
not being assignable toundefined
.The TypeScript lib definitions also leak void into assignments, with
Promise.resolve()
called with no arguments being inferred asPromise<void>
, and the promise constructor explicitly requiringundefined
to be passed to theresolve()
function if the promise’s type parameter isundefined
and notvoid
.All these details make trying to avoid
void
painful, almost impossible. We could explicitlyreturn undefined
from all non-returning functions and callPromise.resolve(undefined)
to create a non-void Promise, but this violates one of the main goals of TypeScript, which is to produce idiomatic JavaScript. Moreover, it is not a workable solution for library authors who wish to avoid thevoid
type, insofar as it would require libraries to impose the same requirements of explicit usages ofundefined
on their users.Here’s the thing. If we were to do a survey of all the places where the
void
type is currently inferred, I would estimate that in >95% of these positions, the actual runtime value is alwaysundefined
. However, because we can’t eliminate the possibility thatvoid
is being used likeunknown
, we’re stuck leaving the voids in place. In essence, TypeScript is not providing the best possible inference for code wherevervoid
is inferred. It’s almost as if TypeScript is forcing a nominal type alias forundefined
on developers, while most TypeScript typing is structural.Beyond producing unnecessary semantic errors, the
void
type is very confusing for JavaScript developers. Not many developers distinguish the return type of function declarations from the return type of function types as two separate type positions, andvoid
is (probably?) the only type whose behavior differs based on its usage in these two places. Furthermore, we see evidence of this confusion whenever people create unions withvoid
likePromise<void> | void
orvoid | undefined
. These union types are mostly useless, and hint that developers are just trying to get errors to go away without understanding the vagueries of void.The Solution
To allow the void type to be optional, I propose we make the following changes to TypeScript.
Allow functions which have a return type of undefined or unknown to return implicitly.
Currently, attempting to type the return types of functions with implicit returns as
undefined
orunknown
will cause the errorA function whose declared type is neither 'void' nor 'any' must return a value.
Theany
type is unacceptable because it makes functions with implicit returns type-unsafe, so we’re left withvoid
. By allowingundefined
orunknown
returning functions to return implicitly, we can avoidvoid
when typing implicitly returning functions.This behavior should also extend to async and generator functions.
Related proposal #36288
Infer functions with implicit returns and no return type as returning undefined.
There are many situations where we would rather not have
void
inferred. For instance, when using anonymous callbacks with promises, we can unwittingly transformPromise<T>
intoPromise<T | void>
rather thanPromise<T | undefined>
.I propose that all functions with implicit returns should be inferred as returning
undefined
when no return type is provided.This behavior should also extend to async and generator functions.
Related proposal: #36239
Infer Promise.resolve() as returning Promise<undefined> and remove any other pressing usages of void in lib.d.ts.
I’m not sure if there are other problematic instances of
void
in TypeScript’s lib typings, but it would be nice if the idiomaticPromise.resolve()
could just be inferred asPromise<undefined>
. We could also change callback types to useunknown
instead ofvoid
but that change feels much less necessary to avoidvoid
.Allow trailing parameters which extend undefined to be optional.
One problem with the semantic changes above which I haven’t mentioned relates to yet another unusual
void
behavior. When trailing parameters in function types extendvoid
, they become optional. This kind of makes sense because, for instance, it allows people to omit the argument to theresolve()
function of a void promise, or the argument to thereturn()
method of a void returning generator. Because of this, inferringundefined
where we used to infervoid
might inadvertently change the required number of arguments to functions, causing type errors where previously there were none.Therefore, I think if we were to consider the above proposals, we should also strongly consider changing TypeScript to infer trailing parameters which extend
undefined
as being optional.This might be a more controversial change which bogs down the larger project of avoiding void, but I think we could reasonably implement the earlier proposals without making this change. Furthermore, I also think this change would be beneficial to TypeScript in a similar way to making
void
optional. Ultimately, I think the larger theme at play is that TypeScript has too many special ways to indicate undefined-ness which don’t affect the runtime and don’t provide any additional type safety.These changes might seem sweeping, but the inflexible non-assignability of
void
should make the changes not as hard to upgrade into as they might seem. Moreover, the benefit to the TypeScript ecosystem will be that we can start refactoringvoid
out of our type definitions. By replacingvoid
withundefined
andunknown
, we can start being more explicit about what exactly our functions return and what we want from our callback types without losing type safety.Furthermore, I think these changes are in line with TypeScript’s design goals, specifically that TypeScript should “3. Impose no runtime overhead on emitted programs” and “4. Emit clean, idiomatic, recognizable JavaScript code.” By allowing
undefined
to model implicit returns and optional trailing parameters, we’re merely describing JavaScript as it is, and the use ofvoid
to model these values can be surprising.☝️ Potential Objections
Lots of people will object and say that
void
is actually a useful type, that the idea of an opposite “dual” ofany
is interesting or whatever. I don’t care for this kind of astronaut analysis and I don’t think thevoid
type is useful, but any such arguments are immaterial to this proposal. My main problem isn’t that thevoid
type exists, but that it is forced upon me. To be clear, none of my proposals change the behavior of thevoid
type, nor do they seek the removal of thevoid
type from TypeScript. I just wantvoid
to stop being inferred or required when I write idiomatic JavaScript. I believe that even if my proposals were implemented, people who actually cared aboutvoid
could continue to explore the unusual features of this gross type and not know anything had changed.People may also object that they still want to use the
void
type, because its inflexibility is useful for typing callbacks, as a guarantee to callback providers that the return value of the callback is never used. I would argue that for the callback case,void
is completely superseded byunknown
, and that you can useunknown
in the place ofvoid
when typing first-class functions today without much friction. Furthermore, as the callback provider, if you really wanted to make sure that a callback’s return value is never used, the most idiomatic and foolproof way to do this in JavaScript would be to use thevoid
operator in the runtime (() => void hideMyReturn()
). Ironically, TypeScript infers this function as returningundefined
and notvoid
.🔍 Search Terms
void, undefined, implicit return, non-returning function, noImplicitReturns
💣 Relevant Errors
✅ Viability Checklist
My suggestion meets these guidelines:
🔗 Selected Relevant Issues
Earliest reference to the
void
type.Some informative discussions on what maintainers think
void
should do.Void in generator types as it relates to the
return()
method.void
assignments like normal ones #33420Documentation of some inconsistencies of function types which return
Promise<void>
, indicating that void isn’t as well-baked as some people seem to think it is.Issue which highlights the general confusion surrounding
void
.undefined
, notvoid
#36239Proposal to infer non-returning functions as returning
undefined
.undefined
return type #36288Proposal to allow functions whose return type is
undefined
to return nothing.Proposal to make
void
properties optional, which would further entrenchvoid
.The text was updated successfully, but these errors were encountered: