TypeScript has a bunch of type annotations that help shape how we think about code. One of the more confusing ones is never
. You don’t really work with it directly, and it’s not something you see in most codebases unless you're paying close attention. It shows up when TypeScript decides something can't happen, like a function that never returns, a case in a switch that shouldn't be reached, or a type that shouldn't exist in a given context.
How TypeScript Uses never
Behind the Scenes
The never
type plays a very specific part in how TypeScript handles logic and flow control. It’s not something most developers write directly when building features, but the compiler relies on it a lot during type checking, especially when it’s working out which paths in the code should go nowhere. If TypeScript sees that a value can’t possibly happen or that a section of code won’t finish in a way that returns anything, it brings in never
. This helps catch issues early by pointing out paths that don’t make sense based on how the types are written.
What never
Represents Internally
never
doesn’t mean empty or null. It means no value exists at all. It’s the most specific type in TypeScript, assignable to everything but only compatible with itself.
Take this function as an example:
It throws an error immediately and never returns a value. TypeScript knows that and marks the return type as never
, because there’s no way to continue once the function is called.
Another case looks like this:
This one just runs forever without hitting a return or throwing. That endless loop gives the same result. The compiler sees that no return value is coming and types the result as never
. Both of these show how never
tracks what can’t happen. It’s not about what values you’re passing around, but what outcomes the compiler knows you won’t reach.
Assignability Rules Involving never
This type only flows one way. You can take never
and pass it into anything else, but you can’t send anything into a spot that expects never
, unless the value itself is also never
. That rule helps the compiler shut down logic that would allow impossible states.
Here’s a switch block that shows how this helps catch mistakes:
If someone adds a new mode like "auto"
to the Mode
type and forgets to update the function, the compiler flags it. It refuses to let that new value land in a spot that expects never
. That’s the whole point. It gives you a hard stop right where you’d want to fix the code anyway.
That flow also blocks you from letting unexpected values fall through quietly. It forces you to match your logic with your types, especially when working with unions or conditional branches that are supposed to be fully covered.
Where It Appears During Type Checking
never
often shows up when TypeScript narrows types down and finds that a branch no longer has anything in it. It can infer that nothing remains, so it quietly uses never
in the background. This happens most often in type guards, switch statements, and narrowing logic that works through union types one branch at a time.
Take this setup:
If a third shape gets added to Payload
, the compiler spots it right away. It can’t let the new structure fall into the last else
block, because that block is supposed to be impossible. Assigning to a variable with the never
type triggers an error if the value doesn’t match the type definition exactly.
That behavior shows up outside of switch statements too. Here’s an example with number values instead of objects:
This one is a bit forced, but you can imagine a version where the value
parameter has already been narrowed in some way, and the compiler is sure that every valid case has been handled. If it believes there’s no valid value left, it treats the branch as unreachable and expects never
. If that’s not true and the code can still reach that spot, you’ll get a type mismatch and a chance to fix the logic before something breaks.
This is where never
shows its purpose. It gives the compiler a way to say, “Nothing should land here.” And when something does, it shows up as a clear signal that the logic or the types need to be revisited. That kind of pressure at compile time helps keep the type system honest and catches changes early that could break things down the line.
Practical Uses and Defensive Benefits
The more you work with never
, the more you start to see how it acts as a quiet safety net in TypeScript. It’s not about writing more code. It’s about making the code you already have harder to break without realizing it. The value it brings really shows when you're dealing with unions that might grow over time, backend responses that evolve across releases, or functions that rely on strict types to catch logic errors early.
Preventing Runtime Bugs with Exhaustiveness Checks
When you’re working with unions, there’s always a chance that someone adds a new value later on without updating every part of the code that depends on it. That silent mismatch is where small bugs creep in. Using never
with exhaustiveness checks gives the compiler a way to catch places where logic falls out of sync with the types.
Here’s a shape-based example that shows how this works in practice:
If someone later adds a "hexagon"
type and forgets to touch this function, the compiler doesn't let that slide. It stops the build and points right at the part that doesn't make sense anymore. Instead of quietly returning the wrong number or skipping the calculation, the error forces a proper update. That helps keep logic close to the types it depends on, which cuts down on mismatches that only show up later.
Keeping APIs Safer with Never for Impossible Values
APIs don’t always behave the way you expect. Sometimes changes show up without warning. You might get back a new kind of response that your app wasn’t built to handle. That’s where never
can help you catch unexpected values before they get used in ways that don’t match your expectations.
Take this case where different access levels are defined:
If the backend introduces a new kind like "guest"
and nobody updates this switch block, TypeScript makes it clear that the new case doesn’t belong here. That one line with never
tells the compiler that anything falling through to the default branch isn’t valid. And if something does, it means there’s a mismatch between the code and the shape it was built to handle. You stop the mistake at build time, not days later after someone runs into the issue in production.
This check is useful anywhere you’re processing known variants. It’s not about writing extra logic. It’s about flagging places where logic might be missing entirely.
Safer Handling of External Inputs and API Keys
Not all credentials are the same. API keys are often tied to an application, while session tokens and OAuth credentials belong to users. That distinction matters when you’re building systems that need to treat application requests differently from user activity. Letting those lines blur can lead to bugs or worse, security holes that are hard to trace.
TypeScript helps keep those cases separate by forcing you to write code that respects the shape of each authentication method. Here’s a union that spells out the difference between app and user authentication:
If the API adds a new method and no one updates this function, the compiler flags it. That error keeps things honest. It means a new authentication flow can’t sneak in and trigger behavior meant for something else. You catch the missing logic early, before it reaches production code that assumes everything’s been handled.
In systems where both users and services interact with the same APIs, you need sharp lines between what a token lets you do. Mixing those up can expose endpoints to the wrong context. With never
, you’re asking the compiler to catch those mix-ups before they become security issues. When everything matches, the code runs as expected. When something falls out of line, you get a clear message pointing right to the problem.
Conclusion
The never
type shows up when TypeScript needs a way to represent the impossible. It gets assigned when code shouldn’t continue, when values can’t exist, and when logic branches are fully covered. You don’t always write it yourself, but the compiler leans on it to keep type checks sharp and consistent. When it appears, it’s usually because something changed or drifted out of sync. That little nudge helps catch those spots early, before they turn into real problems in your code.