Description
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 infer undefined
in more places where void
is currently used. I’ve argued in multiple places that TypeScript’s void
type is confusing and detrimental to development, and I was asked by a maintainer to create the following document to outline the problems with void
and propose potential solutions.
The Problem
First, what is void
? According to the TypeScript handbook:
void is a little like the opposite of any: the absence of having any type at all. You may commonly see this as the return type of functions that do not return a value.
In practice, the void
type is more or less a black hole kind of type. Nothing is assignable to void
except itself, undefined
and any
, and void
is not assignable to anything except itself, unknown
and any
. Additionally, typical type narrowings like typeof x === "string"
do not narrow void
, and void
infects type unions like void | string
by making the resulting type behave like void
.
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 return undefined
, maintainers argued for modeling these functions as returning void
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 of undefined
, you could do so by explicitly returning undefined
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. While void
behaves mostly like a special type alias for undefined
when used in typical assignments, it behaves more like unknown
when used as the return type of a function type (() => void
).
// these functions should return implicitly or will error
function fn1(): void {}
const fn2 = (): void => {};
// only undefined or void can be assigned to void variables
const v: void = undefined;
// void in function types behaves like unknown insofar as it allows the assigned function to return anything
const callback: () => void = () => 1337;
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 the unknown
type nor even strict null checks existed at its point of creation.
Unfortunately, this dual nature of void
as being either undefined
or unknown
based on its position has an undesirable consequence, which is that while undefined
is always assignable to void
, void
is not assignable to undefined
. Personally, I noticed this issue with increasing frequency because of my work with async and generator functions, to the point where I felt that the void
type was getting in my way. These function syntaxes also did not exist when the void
type was created, and their very existence violates the key assumption which motivated the creation of the void
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
like Promise<void>
or Generator<any, void>
, void
types inevitably leak into assignments. I have frequently been frustrated by attempts to assign Promise<void>
to Promise<undefined>
, or Generator<any, void>
to Generator<any, undefined>
; invariably, the error pyramids which TypeScript emits all end with void
not being assignable to undefined
.
class Example {
_initPromise: Promise<undefined>;
async initialize() {
// some async setup code
// no explicit return
}
constructor() {
// A compiler error because initialize is inferred to return Promise<void>.
this._initPromise = this.initialize();
}
}
function *integers() {
let i = 0;
while (true) {
yield i++;
}
}
// A compiler error because value is inferred as number | void.
const value: number | undefined = integers().next().value;
The TypeScript lib definitions also leak void into assignments, with Promise.resolve()
called with no arguments being inferred as Promise<void>
, and the promise constructor explicitly requiring undefined
to be passed to the resolve()
function if the promise’s type parameter is undefined
and not void
.
All these details make trying to avoid void
painful, almost impossible. We could explicitly return undefined
from all non-returning functions and call Promise.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 the void
type, insofar as it would require libraries to impose the same requirements of explicit usages of undefined
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 always undefined
. However, because we can’t eliminate the possibility that void
is being used like unknown
, we’re stuck leaving the voids in place. In essence, TypeScript is not providing the best possible inference for code wherever void
is inferred. It’s almost as if TypeScript is forcing a nominal type alias for undefined
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, and void
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 with void
like Promise<void> | void
or void | 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.
// The following function definitions should not cause compiler errors
function foo(): undefined {
}
function bar(): unknown {
}
async function fooAsync(): Promise<undefined> {
}
function *barIterator(): Generator<any, unknown> {
}
Currently, attempting to type the return types of functions with implicit returns as undefined
or unknown
will cause the error A function whose declared type is neither 'void' nor 'any' must return a value.
The any
type is unacceptable because it makes functions with implicit returns type-unsafe, so we’re left with void
. By allowing undefined
or unknown
returning functions to return implicitly, we can avoid void
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 transform Promise<T>
into Promise<T | void>
rather than Promise<T | undefined>
.
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));
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 idiomatic Promise.resolve()
could just be inferred as Promise<undefined>
. We could also change callback types to use unknown
instead of void
but that change feels much less necessary to avoid void
.
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 extend void
, they become optional. This kind of makes sense because, for instance, it allows people to omit the argument to the resolve()
function of a void promise, or the argument to the return()
method of a void returning generator. Because of this, inferring undefined
where we used to infer void
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.
function optionalAdd(a: number, b: number | undefined) {
return b ? a + b : a;
}
// this should not error
optionalAdd(1);
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 refactoring void
out of our type definitions. By replacing void
with undefined
and unknown
, 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 of void
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” of any
is interesting or whatever. I don’t care for this kind of astronaut analysis and I don’t think the void
type is useful, but any such arguments are immaterial to this proposal. My main problem isn’t that the void
type exists, but that it is forced upon me. To be clear, none of my proposals change the behavior of the void
type, nor do they seek the removal of the void
type from TypeScript. I just want void
to stop being inferred or required when I write idiomatic JavaScript. I believe that even if my proposals were implemented, people who actually cared about void
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 by unknown
, and that you can use unknown
in the place of void
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 the void
operator in the runtime (() => void hideMyReturn()
). Ironically, TypeScript infers this function as returning undefined
and not void
.
🔍 Search Terms
void, undefined, implicit return, non-returning function, noImplicitReturns
💣 Relevant Errors
- TS1345: An expression of type 'void' cannot be tested for truthiness.
- TS2322: Type 'void' is not assignable to type 'undefined'.
- TS2355: A function whose declared type is neither 'void' nor 'any' must return a value.
- TS7030: Not all code paths return a value.
✅ 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.
🔗 Selected Relevant Issues
- [Errors] No error for function with return type annotation that lacks a return statement #62
Earliest reference to thevoid
type. - [documentation] Clarify the semantics of void #20006
Some informative discussions on what maintainers thinkvoid
should do. - Generators/Async Generators with TReturn type argument which extends undefined have a return method with a non-optional argument in 3.6.2 #33357
Void in generator types as it relates to thereturn()
method. - Types for async functions do not handle
void
assignments like normal ones #33420
Documentation of some inconsistencies of function types which returnPromise<void>
, indicating that void isn’t as well-baked as some people seem to think it is. - Optional chaining not working with void type #35850
Issue which highlights the general confusion surroundingvoid
. - Infer non returning function to be returning
undefined
, notvoid
#36239
Proposal to infer non-returning functions as returningundefined
. - Allow non returning functions to have contextually required
undefined
return type #36288
Proposal to allow functions whose return type isundefined
to return nothing. - Treat void-typed properties as optional #40823
Proposal to makevoid
properties optional, which would further entrenchvoid
.