Skip to content

#AvoidTheVoid Allow users to avoid the void type. #42709

Open
@brainkim

Description

@brainkim

Suggestion

Allow developers to avoid the void type.

AVOIDTHEVOID

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions