In the TypeScript helper `function assertNever(value: never): never { throw new Error('Unexpected: ' + JSON.stringify(value)); }`, why is the parameter typed `never`, and why is the return type annotated `never` as well?
answer
- two annotations, two different jobs
- direction of assignability matters
- what fits into an empty type
- what a never return lets callers write
- explicit annotation, not inference
basics
~20 sThe never parameter accepts only a value the checker proved impossible, which is what turns a forgotten case into a compile error. The never return type is assignable to any declared return type and tells control-flow analysis the call terminates.
solid answer
~50 sThey do two different jobs. The parameter is the check: `never` has no values, and nothing is assignable to it except an expression the checker has itself narrowed to `never`, so the call compiles only while the dispatch is genuinely exhaustive — otherwise you get "Argument of type ... is not assignable to parameter of type 'never'". The return type is about how callers can use the call. Because `never` is assignable to every type, `return assertNever(x)` satisfies a function declared to return `string` or `number` with no cast. And control-flow analysis treats a call to a function *explicitly annotated* `: never` as terminating the flow, so the compiler knows code after it is unreachable and does not complain about a missing return. Rely on the annotation, not on inference — the analysis requires the explicit `never` return type.
code
typescript · 22 linestype Result =
| { status: 'ok'; data: string }
| { status: 'error'; message: string }
| { status: 'pending' };
function assertNever(value: never): never {
throw new Error(`Unexpected: ${JSON.stringify(value)}`);
}
function render(result: Result): string {
switch (result.status) {
case 'ok':
return result.data;
case 'error':
return result.message;
default:
// @ts-expect-error 'pending' is still unhandled, so the value is not never
return assertNever(result);
}
}
console.log(render({ status: 'ok', data: 'hi' }));go deeper
Recall the shape of the helper and that the parameter is what the compiler checks. Say that never means no value can exist, so only a branch the compiler proved unreachable can call it.
State the assignability rule in both directions and split the two annotations: parameter enforces exhaustiveness, return type lets the call sit in a return position and ends control flow. Mention that the never return must be written explicitly.
Point out where the reachability analysis stops helping — indirect call targets, inferred return types — and how you keep the helper's runtime message useful enough to identify the producer that sent the bad tag.
Decide whether this convention is enforced by review, by a lint rule, or by structuring dispatches so the compiler catches gaps without a helper at all, and what that costs teams consuming your unions across package boundaries.
## Two annotations, two independent jobs The helper is four lines and every one of them is load-bearing, but the parameter type and the return type solve unrelated problems. A candidate who explains only one has half the answer. ```ts function assertNever(value: never): never { throw new Error(`Unexpected: ${JSON.stringify(value)}`); } ``` ## The parameter: `never` as a proof obligation Assignability decides what you may pass. `never` is the type with no members, and the assignability rule that matters is directional: `never` is assignable to every type, and *no* type is assignable to `never` except `never` itself. That asymmetry is what makes the parameter a check rather than a formality. So `assertNever(value)` compiles exactly when the compiler's control-flow analysis has already concluded that no value can reach that point. In an exhaustive dispatch over a discriminated union, the residual type after all members are handled is `never`, and the call is accepted. Leave one member unhandled and the residual type is that member, producing: ``` Argument of type '{ kind: "pending"; }' is not assignable to parameter of type 'never'. ``` The diagnostic even names the case you forgot, which is why the error is so pleasant to act on. Any wider parameter type destroys this. `unknown` accepts everything, so the call is always green. `any` is worse — it accepts everything *and* suppresses errors around it. The union type itself (`value: Shape`) also always compiles, since the narrowed residual type is a subtype of the union. Only `never` is narrow enough to ask a question the compiler can answer with an error. ## The return type: assignability plus reachability The declared return type does nothing for the exhaustiveness check. It buys two conveniences at the call site. **Assignability into any position.** `never` is assignable to every type, so the call can stand wherever a value is expected: ```ts function area(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'square': return shape.side ** 2; default: return assertNever(shape); // never satisfies number } } ``` Without the `never` return type — say the function were annotated `: void` — that `return` would be a type error, and you would have to write the call and a separate dummy return. **Reachability.** TypeScript's control-flow analysis knows that a call to a function whose *declared* return type is `never` does not return, so statements after it are unreachable and the function's end point is not reachable through it. That is what lets you write the call as a bare statement in a value-returning function without a "lacks ending return statement" error: ```ts function area(shape: Shape): number { if (shape.kind === 'circle') return Math.PI * shape.radius ** 2; if (shape.kind === 'square') return shape.side ** 2; assertNever(shape); // flow ends here; no missing-return error } ``` This analysis has a precondition worth knowing: it applies only when the called function has an **explicit** `never` return-type annotation, and when the call target is a plain identifier or a dotted name (`utils.assertNever(x)`), not an arbitrary expression. Annotate the helper; do not lean on inference, and do not route the call through an indirection like `handlers.get('fail')!(x)` and expect the flow analysis to follow. ## The runtime half The body throws, and the argument is stringified into the message. Both choices are deliberate. Types are erased at compile time, so if a value ever does reach this call — a payload with an unrecognised tag, a stale persisted record, an `as` assertion upstream — the helper is the only thing standing there. Printing the offending value is what makes the resulting log actionable; `Unexpected value` alone tells you nothing about which producer went off-contract. Note that `JSON.stringify(value)` is typed fine here because `never` is assignable to that parameter. Some teams prefer `String(value)`; either works, and the choice only affects log readability. ## What this is not This is not an assertion function. The `asserts value is T` form is a separate signature shape with its own rules about narrowing the *caller's* variable. `assertNever` does no narrowing — it consumes a value the caller has already narrowed to nothing. It is also not built in. TypeScript ships no `assertNever`; you write it, typically once in a shared module, and the name is a convention (`assertUnreachable` is equally common). ## What interviewers listen for The strong answer states the assignability direction explicitly — *nothing is assignable to never* — rather than hand-waving about never being "impossible". It separates the parameter's role (the check) from the return type's role (usability and reachability), and it mentions that the reachability behaviour depends on the explicit annotation.
- Would the helper still work if you left the return type off and let TypeScript infer it?The exhaustiveness check still works, because that depends only on the parameter. What you lose is the call-site ergonomics: control-flow analysis credits a call as terminating only when the callee has an explicit `never` return-type annotation, so a bare `assertNever(x);` at the end of a value-returning function can start reporting a missing return. Annotate it.
- Why can `return assertNever(x)` satisfy a function declared to return `number`?Because `never` is assignable to every type — it is the bottom of the assignability lattice, so an expression of type `never` fits wherever any value is expected. No cast is needed. This is the same rule read in the opposite direction from the one that makes the parameter a check.
- What breaks if you write the parameter as `value: any` to silence a compile error?You delete the check. `any` accepts every argument and suppresses surrounding errors, so the dispatch compiles forever regardless of how many members go unhandled, and the helper degrades into an ordinary runtime throw. If the call is failing to compile, the correct response is to handle the case it names.
saying these in an interview costs you the question
- Saying the return type is what enforces exhaustiveness
- Typing the parameter as the union or unknown and expecting a check
- Claiming never is assignable from every type
- Assuming the compiler infers never for a throwing function declaration
- Calling it an asserts value is T assertion function