← Back to Blog

TypeScript's never Type: The Most Misunderstood Keyword in the Language

2026-03-26 typescripttypestutorial

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 never

If 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;  // never

There'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;   // ✅ fine

This 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.

Got thoughts on this post?

I'd love to hear from you. Reach out on any of these:

Want to learn by doing?

ByteLearn has free courses with interactive quizzes on JavaScript, TypeScript, Svelte 5, and more.

Browse courses →
© 2026 ByteLearn.dev. Free courses for developers. · Privacy