Topic 2.3
void, never and Returning Nothing
In one line
void means a function gives back nothing you should use, and never means a function never gets back at all because it throws or loops forever. Both help the checker understand what happens after a call.
Think of it like this
A postbox and a trapdoor. You post a letter and walk away; nobody hands you anything back, and you don't expect them to. That is void: the job is done, there is nothing to use. A trapdoor is different: step on it and you never come back out the other side. That is never: the code after the call can't be reached.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- void
- A return type meaning "no useful result". The caller must not use what comes back.
- undefined
- A real value meaning "nothing here". It can be returned and checked, unlike a void result.
- never
- The type of a value that can't exist. A function returns
neverwhen it always throws or never ends. - Callback
- A function you pass to another function so that it can be called later.
- Side effect
- Something a function does besides returning a value, such as printing or saving.
- Unreachable code
- Code that can never run because everything before it throws or never ends.
- Bottom type
- A type with no values at all.
neveris the bottom type: it fits into every other type. - Throw
- Stop the function right now and hand an error up to whoever called it.
Step by step
01void: it does something, it gives nothing
A function that prints has no result to use. Its return type is void, written by you or inferred when there is no return value.
The call has the type void. You can call it as a statement, but there is nothing to store.
function logIt(message: string): void {
console.log(message);
}
const r = logIt("hi"); // r has the type void02A void callback may return something
The callback type () => void means "I will ignore what you return". So a function that returns a number fits.
That is why forEach accepts (n) => out.push(n) even though push returns the new length.
type Callback = () => void;
const cb: Callback = () => 42; // fine
const v = cb(); // v: void (the 42 is still there at runtime)03never: this function does not come back
A function that always throws has the return type never. Write it on purpose to make a reusable "stop here" helper.
After a call to it, TypeScript knows the code below is unreachable on that path.
function fail(message: string): never {
throw new Error(message);
}04Using never to narrow
In must, if the value is undefined we call fail. The checker sees that this branch never continues, so below the if the value is a string.
This is the same idea as an early return or throw, but wrapped in a function with a good error message.
05never in the type system
never fits into every type but nothing fits into never except never. In a union it vanishes (string | never is string).
A function that promises never must never reach its end. If it can, tsc says TS2534.
Try it yourself
- 1
Make the callback strict
In the second example change
type Callback = () => void;totype Callback = () => undefined;. Predict: does() => 42still fit? Read the new error.voidis the lenient one. - 2
Remove the never
In the third example change
fail(message: string): nevertofail(message: string): void. Predict what happens tomust. The error moves toreturn value:TS2322: Type 'string | undefined' is not assignable to type 'string'.WithoutneverTypeScript thinks the code can continue afterfail, sovaluemay still beundefined. - 3
Use a void result
After the
forgotfunction in the first example, addconst total: number = logIt("x");. Read the message and explain why using avoidresult as a value is blocked.
Code & diagrams
logIt's result has the type void. giveUndefined really returns the value undefined. forgot() has no return at all and is accepted (TypeScript 5.1). wrongVoid is the only error: a declared void can't return a number.
Console
(nothing was printed)Expected console output
hi
undefined undefinedThe 42 is really returned (the console shows it), but TypeScript gives v the type void, so you can't use it as a number. map is different: it keeps what the callback returns, so the array is void[].
Console
(nothing was printed)Expected console output
42
[ 2, 4, 6 ]must returns a string on every path because fail never comes back; change fail's return type to void and read the new error. The arrow function that throws is inferred as never, but the function declaration is inferred as void. broken promised never and reaches its end.
Console
(nothing was printed)Expected console output
ready
Error: a value is requiredA never value fits anywhere (n is a number), but a 5 doesn't fit into never. In the union string | never the never part disappears.
Console
(nothing was printed)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Return a value from a void function
Look at wrongVoid in the first example: a function declared : void that does return 42;. (Change : void to : number and the error disappears.)
Break #2
A never function that comes back
Look at broken in the third example: function broken(): never { console.log("returns"); } (its body ends without throwing).
Myth vs fact
Myth
void and undefined mean the same thing.
Fact
undefined is a value you can return and check. void means "ignore the result". undefined fits into void, but a void result is not assignable to undefined.
Myth
A callback declared () => void must not return anything.
Fact
It means the caller ignores the result, so any function fits. That is why forEach(n => out.push(n)) compiles.
Myth
A function that only throws is automatically never.
Fact
Only arrow functions and function expressions are inferred as never. A function declaration that throws is inferred as void. Write : never to be explicit, and to make the call affect control flow.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
For a call to a
neverfunction to narrow control flow, TypeScript needs to know its type without looking at the call: the function must be a declaration, or aconstwith an explicit type annotation. Aconst fail = (m: string) => { throw ... }with no annotation gets the inferred type(m: string) => never, but calls to it do not narrow the code below, because the checker only trusts declared types here. - ▸
The leniency of
() => voidis a special case in assignability: a function type with avoidreturn accepts any return type. It is also why a() => voidcallback accepts anasyncfunction (which returns aPromise), and a rich source of subtle bugs when aPromiseis returned from avoidcallback and nobody awaits it. - ▸
neveris the empty union. That is whyExclude<T, U>and distributive conditional types remove cases by turning them intonever(Topic 10.2), and why an exhaustiveswitchcan leave a variable of typenever(Topic 4.6).
Remember this
- 1
The problem this solves: Some functions give back nothing useful, and some never return at all. If the types can't say this, someone uses a result that isn't there, or a code path is quietly forgotten. Many functions do something (print a line, save a file, change a page) and have no result to give. In JavaScript they return
undefinedby accident. TypeScript has a type to say "don't use the result": **void**. A function that prints and ends has the return typevoid, and you can write it yourself:function logIt(message: string): void. - 2
voidandundefinedare close but not the same.undefinedis a real value that a function may return and you may use.voidsays "whatever comes back, ignore it". You can assignundefinedto avoidvariable, but not the other way round:const a: undefined = logIt("x")isTS2322: Type 'void' is not assignable to type 'undefined'. Usevoidfor "returns nothing"; useundefinedonly when the function really returns that value and the caller may look at it. - 3
A function declared
: voidmay notreturn 42: that isTS2322: Type 'number' is not assignable to type 'void'. But there is a deliberate exception for function types: a function whose type says it returnsvoidmay be given a function that returns something.const cb: () => void = () => 42is fine, and so isnames.forEach((n) => out.push(n))even thoughpushreturns a number. The caller promises to ignore the result, so it is safe, and without this rule you could not pass most functions where a callback is expected. - 4
The exception has a catch. The result is still a real value at runtime; TypeScript just stops you from using it, because its type is
void.const v = cb()has the typevoid, and at runtimevis42. Don't rely on that: treatvoidresults as unusable. Also, only function types and contexts like callbacks get this leniency. A function declaration with an explicit: voidreturn type is still strict. - 5
**
never** is the type of something that can't exist: a value that never happens. A function returnsneverwhen it can't finish normally: it alwaysthrows an error or loops forever.function fail(message: string): never { throw new Error(message); }. Writingnevermakes TypeScript check you: a function that saysneverbut can reach its end isTS2534: A function returning 'never' cannot have a reachable end point. - 6
neverhelps the checker understand the rest of your code. After a call to a function that returnsnever, TypeScript treats the code below as unreachable, so afterif (value === undefined) { fail("missing"); }the value is known to be astring. This only works when the function'sneveris written explicitly (a declared function or a variable with an annotated type); a function declaration that just throws, without the annotation, has the inferred typevoid, while an arrow function or function expression that always throws is inferred asnever. - 7
neveris also the bottom type: it is assignable to every other type (there is no value to be wrong about) and nothing butnevercan be assigned to it (let v: never; v = 5is an error). In a union it disappears, since it adds no possible values:string | neveris juststring. You'll use this in Topic 4.6 (exhaustiveness checks) and in conditional types much later.
Explain it without notes
What is the difference between void and undefined as a return type?
Why can forEach accept a callback that returns a number?
What does the never return type mean, and when do you use it?
What is never in the type system, and why does string | never equal string?
Practice
Write function notify(message: string): void that logs the message. Try to log its result into a string variable and note the error. Then write a callback of type () => void that returns a string and show that it type-checks.
Write function assertNever(value: never): never that throws an error with the text Unexpected: and the value. Use it to finish a function size(shape: "small" | "large"): number with an if per case.
Write function mustGet(prices: Map<string, number>, item: string): number that returns the price, or calls a never helper fail when the item is missing. Explain why it type-checks although Map.get can return undefined.
Trade-offs
- ↔
The lenient
voidcallback rule makes callbacks easy to write, but it also hides cases where you returned a value that you expected to be used (like aPromisenobody awaits). Linters (no-misused-promises) catch that. - ↔
A
never-returning helper centralises error messages and keeps happy-path code flat, but it is invisible in the signature of the callers, so readers must knowfailalways throws. Name it so that it is obvious (fail,assertNever,unreachable).
Done when you can
I can explain
voidversusundefinedin one sentence.I can explain why a callback typed
() => voidaccepts a function that returns a number.I can write a
neverfunction and explainTS2534.I can say what is inferred for an arrow function and a function declaration that only throw.
I can explain why
string | neverisstring.