Lesson 11 · Module 1.6 — Core type vocabulary
never type — exhaustiveness, and the key that disappearsHere it is in one line: the never type has two behaviors.
In the value world it powers exhaustive switch checks; in the type world it makes a
mapped-type key vanish (because string | never = string). Same type, two faces — and
both show up constantly in real code.
never is the empty typeRead never as “a type with no possible values.” Nothing is assignable
to it (no value can be one). That single fact explains both behaviors below.
switch checkingA classic pattern — assign the “impossible” branch to a never so the compiler
forces you to handle every case:
type Shape = { kind: "circle" } | { kind: "square" };
function handle(shape: Shape): string {
switch (shape.kind) {
case "circle": return "circle";
case "square": return "square";
default:
const _exhaustive: never = shape; // ✅ only compiles if no case is left
return _exhaustive;
}
}
By the default branch, TypeScript has narrowed shape down to
never — there’s nothing left it could be. Assigning it to _exhaustive: never
succeeds. But the day someone adds { kind: "triangle" } to Shape, that
branch can now be reached with a real value, shape is no longer never, and
the assignment fails to compile — a free reminder to handle the new case. Read
const _x: never = shape as “prove to me nothing can reach here.”
The other behavior worth flagging: in a union, never contributes nothing —
string | never is just string. Key remapping in a mapped type exploits this
to drop a key:
type User = { name: string; age: number; password: string };
type PublicUser = {
[K in keyof User as K extends "password" ? never : K]: User[K]
};
// Result: { name: string; age: number } — "password" remapped to never, so it's gone
Read the as clause as “rename each key K to… itself, unless it is
"password", in which case rename it to never.” And a key of type
never is filtered out of the result — exactly because never vanishes from a
union. That is how mapped types delete fields.
never”Here is the error that confuses everyone. Picture an object whose two properties have different types, and a function that writes to one of them through a key that could be either one:
const counts = { cats: 0, dogs: "lots" }; // cats: number, dogs: string
function set(key: "cats" | "dogs", value: number | string) {
counts[key] = value; // ❌ Type 'string | number' is not assignable to type 'never'
}
Reading counts[key] is fine — you get string | number back. Writing
is the trap. Because key might be "cats" (which needs a number)
or "dogs" (which needs a string), the value you assign has to be valid for
both at once — a number and a string. No value is both, so
that combined type is never (the empty type from the top of this lesson). Nothing fits, and TS says
“not assignable to never.”
Read “not assignable to never” as “the compiler narrowed this slot down to
the empty type, so nothing can go in.” Same never as before — just showing up on the
write side this time.
never = the type with no values. Assigned to a value (const _x:
never = shape) → an exhaustiveness guard. Produced by a mapped-type key → that key
is dropped. Seen in an error → the compiler narrowed something to nothing.
Recall, don’t re-read. (Answers reveal on click.)
const _x: never = shape in a default branch enforce exhaustiveness?never via as causes that key to…For the exhaustiveness pattern, the handbook page is
Narrowing — Exhaustiveness Checking.
For the key-dropping behavior, see
Mapped Types — Key Remapping via as.
Best way to feel it: paste the Shape switch into the
TS Playground, add a third kind, and
watch the _exhaustive line light up red.
Hit a stray never in an error and can’t tell which face it is? Ask yourself
“value-world guard, type-world drop, or a narrowing dead-end?” This closes Module 1 — next is
Module 2 on generics, where these vocabulary pieces start composing.