4.1 void: Functions That Return Nothing
Right, let’s talk about void. It sounds so final, doesn’tt it? Like a cosmic emptiness. But in TypeScript, it’s far less dramatic and far more practical. I use void for one specific job: to annotate a function that doesn’t return a useful value. It’s the type system’s way of saying, “This function is called for its side effects, not for what it gives back.”
Think of a function that updates the DOM, writes to a console, or saves data to a database. You don’t call console.log('error!') because you want to use its return value (which is undefined, by the way); you call it because you want the effect of a message appearing in your console. That’s the void territory.
Here’s the most basic, almost tautological, example:
function logMessage(message: string): void {
console.log(message);
// No return statement? Perfect. That's the point.
}
const result = logMessage("Hello, void!"); // result is of type `void`
If you try to use that result variable for anything, TypeScript will rightly complain. It’s like trying to drink from an empty cup.
The Subtle But Critical Difference Between void and undefined
This is where people’s eyebrows furrow. “Wait,” you say, “the function didn’t return anything, so isn’t its return value undefined?” At the value level, at runtime, in JavaScript, you are absolutely correct. The result variable in the example above is undefined.
But TypeScript operates at the type level. void and undefined are not the same thing in this context. void is a special type we use specifically for function return types to indicate the return value is intentionally ignored. undefined is a literal value and a type.
The key distinction is in what you’re allowed to do. A function typed to return undefined must have an explicit return statement, either return; or return undefined;. A function typed to return void can have any of the following: an explicit return;, an explicit return undefined;, or no return statement at all. It’s more flexible because it’s communicating intent, not enforcing a specific value.
// This is valid. We're saying "this returns nothing important."
function a(): void {
// no return
}
// Also valid for void.
function b(): void {
return;
}
// This is ALSO valid for void, which often surprises people.
function c(): void {
return undefined; // This is fine for void.
}
// But this is INVALID. This function is typed to return undefined, so it MUST do so explicitly.
function d(): undefined {
// Error: A function whose declared type is neither 'void' nor 'any' must return a value.
}
The Curious Case of Contextual Typing
TypeScript has a trick up its sleeve called “contextual typing.” This is where void gets really interesting and a bit quirky. When you assign a function expression to a location that expects a void return type, TypeScript will contextually enforce that your function doesn’t try to return a useful value.
This is a huge deal for array methods like .forEach and .map, which expect a callback that returns void. Look at this perfectly legal but utterly useless code:
const numbers = [1, 2, 3];
// forEach expects a callback that returns void.
numbers.forEach((n) => {
return n * 2; // Wait, I'm returning a number?!
});
// This compiles without a hitch. Why?
It works because the return value of the arrow function is contextually typed as void. The type system sees you’re in a void context and says, “Okay, the caller has promised me they will ignore this return value, so I won’t cause an error.” It’s effectively discarding the returned number. This is a classic pitfall for developers coming from other languages. You meant to use .map to create a new array, but you used .forEach and your returned value just vanishes into the void. The code runs, but it doesn’t do what you intended.
Why This Matters: Preventing Accidental Misuse
The real power of void isn’t just describing your own functions; it’s in consuming functions that expect callbacks. Let’s say you’re using a library function that accepts a callback which is expected to have no return value. The library author has typed its parameter as () => void.
This is a contract. It’s the author saying, “I will call your function, and I promise I will not do anything with its return value.” This is a fantastic safety feature for you! It means you can pass in a function that does return a value, and TypeScript will allow it (thanks to contextual typing), but the author’s function is prevented from accidentally using that value. It creates a clean, safe boundary.
// A library function that expects a callback with no return value
function libraryFunction(callback: () => void): void {
// ... does some work
const result = callback(); // The type of 'result' is `void` here.
// The library author CANNOT use `result` because it's treated as `void`.
// This is a good thing! It prevents them from accidentally relying on your callback's return.
}
// I can pass a function that returns a number, and it's accepted.
libraryFunction(() => {
console.log("I'm called for a side effect");
return 42; // This is allowed contextually, but the return value is ignored.
});
In essence, void is less about what you return and more about what the caller is allowed to do with it. It’s a tool for designing robust APIs and avoiding a whole class of subtle bugs. It’s TypeScript being a brilliant friend, looking at your return n * 2 inside a .forEach and saying, “I’m not gonna stop you, but you should know I’m legally obligated to ignore this.”