You're writing TypeScript, everything's going fine, and then you see it:
Type 'string' is not assignable to type 'never'.Your first reaction is probably "what the hell is never?" You didn't write it. You didn't ask for it. And the error message isn't helping.
Most people Google it, find a one-liner explanation ("a type that represents values that never occur"), nod slowly, and move on without actually understanding it. I know because that's exactly what I did.
But never is one of those things that clicks once you see why it exists, and then you start using it on purpose.
The Opposite of any
Here's the mental model. TypeScript has two extremes:
any: "this could be literally anything, I don't care"never: "this can literally never happen"
any is the top. Everything is assignable to it. never is the bottom. Nothing is assignable to it (except another never). They're opposites.
let anything: any = "hello"; // ✅ fine
anything = 42; // ✅ fine
anything = true; // ✅ fine
let nothing: never = "hello"; // ❌ error, can't assign anything to neverIf any means "I give up on type checking," never means "if we ever get here, something went wrong."
Where You've Already Seen It
The most common place never shows up is in functions that don't return. Not functions that return nothing (that's void), but functions that never finish:
// void: the function finishes, it just doesn't return a value
function log(message: string): void {
console.log(message);
// implicitly returns undefined, the function completes
}
// never: the function never completes
function crash(message: string): never {
throw new Error(message);
// nothing after this line ever runs
}Think of it like this: void is a car reaching its destination and stopping. never is a car driving off a cliff. Both "don't return a value," but for very different reasons.
An infinite loop is another example:
function forever(): never {
while (true) {
// this never ends
}
}The Real Power: Catching Your Own Mistakes
The throw example is fine, but it's not why never matters. The real power is something called exhaustiveness checking, using never to make TypeScript yell at you when you forget to handle a case.
Say you have user roles:
type Role = 'admin' | 'user' | 'guest';
function getPermissions(role: Role): string {
switch (role) {
case 'admin': return "Full access";
case 'user': return "Read and write";
case 'guest': return "Read only";
}
}This works. But what happens six months later when someone adds a new role?
type Role = 'admin' | 'user' | 'guest' | 'moderator';The getPermissions function still compiles. No error. No warning. The new 'moderator' role silently falls through and returns undefined. You won't find out until a user reports a bug, or worse, gets the wrong permissions.
Here's the fix:
function getPermissions(role: Role): string {
switch (role) {
case 'admin': return "Full access";
case 'user': return "Read and write";
case 'guest': return "Read only";
default:
const _exhaustive: never = role;
return _exhaustive;
}
}Now when you add 'moderator' to the union, TypeScript immediately throws a compile error:
Type '"moderator"' is not assignable to type 'never'.Why? Because after handling admin, user, and guest, TypeScript narrows role down to 'moderator' in the default branch. And 'moderator' can't be assigned to never. The compiler is literally telling you: "Hey, you forgot to handle this case."
This is the pattern. You're using never as a safety net that catches future mistakes at compile time instead of runtime.
How TypeScript Narrows to never
This works because of type narrowing. As TypeScript moves through each case, it eliminates possibilities:
function getPermissions(role: Role): string {
// role is 'admin' | 'user' | 'guest'
switch (role) {
case 'admin':
// role is 'admin'
return "Full access";
case 'user':
// role is 'user'
return "Read and write";
case 'guest':
// role is 'guest'
return "Read only";
default:
// role is... nothing. All options exhausted. Type is 'never'.
const _exhaustive: never = role; // ✅ compiles
return _exhaustive;
}
}Every case removes one member from the union. When they're all gone, the only type left is never, the empty set.
The Impossible Intersection
Here's another way never shows up naturally. What's a value that's both a string and a number at the same time?
type Impossible = string & number; // neverThere's no value in JavaScript that's simultaneously a string and a number. So TypeScript represents that as never. It's not an error. It's TypeScript correctly saying "this type has zero possible values."
This matters in practice when you're building complex conditional types or filtering properties. If a type computation results in an impossible state, TypeScript prunes it with never instead of crashing.
never in the Wild: Filtering Object Types
Here's a real-world use. Say you want to extract only the string properties from an object type:
type StringKeysOnly<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
interface User {
id: number;
name: string;
email: string;
age: number;
}
type StringKeys = StringKeysOnly<User>; // "name" | "email"The never values get automatically filtered out of the union. number keys like id and age map to never, and TypeScript drops them. Only "name" and "email" survive.
Why It's Called the "Bottom Type"
In type theory, never is the bottom type. Every type system has one. Rust calls it !, Scala calls it Nothing, Haskell calls it Void.
The bottom type is a subtype of every other type. That sounds weird, but it makes sense: if a value can never exist, it technically doesn't violate the rules of any type. You can assign never to anything:
const n: never = crash("boom");
const s: string = n; // ✅ fine
const num: number = n; // ✅ fine
const b: boolean = n; // ✅ fineThis isn't useful in everyday code, but it's why the type system stays consistent. never is the mathematical zero of TypeScript's type algebra.
Quick Reference
| Situation | Type |
|---|---|
| Function returns nothing | void |
| Function never returns | never |
Impossible intersection (string & number) |
never |
| All union members exhausted after narrowing | never |
| Filtered-out branch in conditional types | never |
The Takeaway
never isn't some obscure academic concept. It's TypeScript's way of representing impossibility, and that turns out to be incredibly useful.
Use it for exhaustiveness checking in switch statements. Understand it when you see it in error messages. And know that when TypeScript narrows a type down to never, it's telling you something important: "This should be unreachable. If it's not, you have a bug."
The developers who understand never write code that catches mistakes at compile time. Everyone else finds them in production.