Skip to main content

Void-Returning Function Assignability

· 5 min read

Most popular programming languages use the void keyword to indicate that a function cannot return a value. Learning TypeScript describes TypeScript's void keyword as indicating that the returned value from a function will be ignored. Those two definitions are not always the same! TypeScript's void comes with an initially surprising behavior: a function type with a non-void return is considered assignable to a function type with a void return.

ts
let returnsVoid: () => void;
 
returnsVoid = () => "this is fine";
ts
let returnsVoid: () => void;
 
returnsVoid = () => "this is fine";

Why is that?

void Is More of an Idea

It's very common for functions to be called and have their return type ignored. Take this code block, in which a function that returns number (because Array's .unshift returns the new array length) is passed to .forEach:

ts
let letters = ["c", "d", "e"];
 
["a", "b"].forEach((letter) => letters.unshift(letter)); // Ok
ts
let letters = ["c", "d", "e"];
 
["a", "b"].forEach((letter) => letters.unshift(letter)); // Ok

We should be allowed to write that code in TypeScript! .forEach's function parameter's return type is correctly marked as void because the value returned from the function is ignored. Functions that return a value can be used in locations that don't care about their returned value.

void is not any

Note that TypeScript will not allow you to return a value within a function explicitly annotated as returning void. TypeScript will report an assignability type error:

ts
function thisIsNotFine(): void {
return "🔥";
Type 'string' is not assignable to type 'void'.2322Type 'string' is not assignable to type 'void'.
// Error: Type 'string' is not assignable to type 'void'.
}
ts
function thisIsNotFine(): void {
return "🔥";
Type 'string' is not assignable to type 'void'.2322Type 'string' is not assignable to type 'void'.
// Error: Type 'string' is not assignable to type 'void'.
}

void is not a free pass to return whatever you want. It's an explicit indication that no return value is meant to be used.

Addendum: Misused Promises

Anat Dagan posted on Twitter:

Maybe I'm missing something, but assigning functions that return non-void values to a void-returning function sounds like an opening for bugs to me…

Indeed, it is! Passing an asynchronous function (one that returns a Promise) to a handler that doesn't handle the created Promise means Promise rejections might not be handled. This frequently shows up with event listeners in UI libraries such as React:

tsx
async function myOnClick() {
// (simulate some asynchronous work happening)
await new Promise((resolve) => setTimeout(resolve, 100));
 
// (simulate some crashing bug in the code)
if (Math.random() > 0.5) {
throw new Error("Oh no!");
}
}
 
// The "Oh no!" Error won't be logged anywhere or handled...
const myButton = <button onClick={myOnClick}>...</button>;
tsx
async function myOnClick() {
// (simulate some asynchronous work happening)
await new Promise((resolve) => setTimeout(resolve, 100));
 
// (simulate some crashing bug in the code)
if (Math.random() > 0.5) {
throw new Error("Oh no!");
}
}
 
// The "Oh no!" Error won't be logged anywhere or handled...
const myButton = <button onClick={myOnClick}>...</button>;

TypeScript ESLint's no-misused-promises rule can catch cases like this automatically for you. The no-floating-promises rule is also useful for catching other classes of "floating" Promises.


This article was inspired by the thread on this tweet: https://twitter.com/haroenv/status/1534186668244230155

Got your own TypeScript questions? Tweet @LearningTSBook and the answer might become an article too!