C.W.K.
Stream
Lesson 03 of 05 · published

`any`, `unknown`, `never`: The Type-System Extremes

~12 min · primitives, any, unknown, never, top-type, bottom-type

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"any is the off switch. unknown is the careful switch. never is the impossible."

Three types that don't behave like the others

Most types are about what a value is. any, unknown, and never are about what the type system itself does. They're meta-tools — escape hatches, safer escape hatches, and impossibility markers — and learning them well separates beginner TypeScript from professional TypeScript.

any — the off switch

any turns the type system off for the value it annotates. Property access doesn't check. Method calls don't check. Assignment to any other type doesn't check. The looseness is contagious — (anyValue).foo.bar.baz all stay any, and you've lost type safety for a whole subtree of access.

The legitimate uses of any are rare: interop with untyped JavaScript libraries (and even then, prefer writing a .d.ts declaration), one-off debugging, or migration of legacy code. In a strict codebase, every any should be flagged by a lint rule and require a comment justifying it.

unknown — the careful switch

unknown accepts any value (just like any) but won't let you do anything with it until you narrow. You can't call methods, can't access properties, can't assign it to a typed variable. You have to use typeof / instanceof / a custom type guard first.

The slogan: unknown is what any should have been. Same flexibility going in, much more safety going out. The Strict Mode track's useUnknownInCatchVariables flag makes catch (e) default to unknown exactly so this safety reaches your error handling.

never — the impossible

never is the empty type. No value is assignable to it. Its uses are very specific and very useful:

  • Return type of a function that throws or loops forever — "this function does not return normally."
  • The else branch in exhaustive narrowing — "this branch is unreachable, prove it."
  • The result of conditional types that mean "this case doesn't apply" — you'll see this in the Type Manipulation track.

If a variable ever has type never when you weren't expecting it, that's the compiler telling you the type-flow has reached an impossible state. It's a signal, not a target.

The hierarchy: any sits outside the type lattice — it's both a top and bottom that the compiler refuses to check. unknown is the true top type (everything is assignable to it). never is the bottom type (nothing is assignable to it, and it's assignable to everything). Once you see them as positions in the lattice, the behavior becomes predictable.

Pippa's confession

When I read cwkPippa code and find an : any, my first reaction is "why?" and my second is "can we make it unknown?" The number of legitimate anys in 30,000 lines of cwkPippa frontend is in the single digits — and each one has a comment explaining why narrowing wasn't possible. never shows up in exhaustive switches and in conditional-type returns. Both are deliberate. unknown is the default for any boundary I don't fully control — incoming JSON, third-party events, anything I parse before validating.

Code

any lets everything through. unknown forces you to narrow.·typescript
// any — the off switch (don't reach for this).
let danger: any = 'hello';
danger = 42;                  // ✅ any accepts anything
danger = { foo: { bar: 1 } };
danger.foo.bar.baz.qux();     // ✅ compiles — and crashes at runtime
                              // The looseness spreads through every access.

// unknown — the careful switch.
let careful: unknown = 'hello';
careful = 42;                 // ✅ assignment OK
careful = { foo: 1 };         // ✅ assignment OK

careful.toUpperCase();        // ❌ Object is of type 'unknown'
careful.foo;                  // ❌ Object is of type 'unknown'

// You must narrow first:
if (typeof careful === 'string') {
  careful.toUpperCase();      // ✅ narrowed to string
}
never as bottom type — three canonical uses·typescript
// never — the impossible.

// 1. Return type for functions that don't return normally.
function throwIt(msg: string): never {
  throw new Error(msg);
}
function loopForever(): never {
  while (true) { /* ... */ }
}

// 2. The else branch in exhaustive switches.
type Shape = { kind: 'circle'; r: number } | { kind: 'square'; s: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.r ** 2;
    case 'square': return shape.s ** 2;
    default:
      // If you add a new variant to Shape and forget a case,
      // this assignment fails at compile time:
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// 3. The hierarchy in action.
const neverVal!: never = undefined as never;
const asString: string = neverVal;  // ✅ never is assignable to anything
const asNumber: number = neverVal;  // ✅ same
// Nothing is assignable INTO never — that's why it's the bottom.

External links

Exercise

Write a function parseJson(input: string): unknown that uses JSON.parse. Note the return type is unknown, not anyJSON.parse is one of the standard-library functions that returns any, and many style guides recommend wrapping it. Then write a parseUser(input: string): User | null that calls parseJson and narrows the result by checking required fields. Notice how unknown forces you to write the validation that any would have let you skip.
Hint
After const parsed = parseJson(input), you can't use parsed.name yet. Write a series of if checks (typeof parsed === 'object', parsed !== null, 'name' in parsed, typeof parsed.name === 'string') before you can return a User. That's the price of safety — and it's worth paying.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.