any vs unknown vs never

Reviewed & published by Brayan K

any disables type checking entirely, unknown accepts any value but forces you to narrow it before use, and never is the empty type for values that can never exist — together they are TypeScript's three "special" types.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Imagine a sealed package arrives. any is ripping it open and using whatever's inside without looking — sometimes fine, sometimes you grab a knife by the blade. unknown is putting the package through an X-ray first: you can't touch the contents until the scan tells you what they are. never is a package that, by the laws of physics, could never have been sent — if your code expects one, something upstream is broken.

1. any: The Off Switch

A value typed any tells TypeScript to stop checking it completely. You can call any method, read any property, and reassign it to anything — including operations that don't exist and will crash at runtime. Worse, any is contagious: anything you derive from an any value is also any, so a single careless annotation can quietly disable safety across a whole file.

// 'any' = "stop checking this value entirely":
let x: any = "hello";
x.toUpperCase();   // ✅ allowed
x.notReal();       // ✅ ALSO allowed (no error!) — crashes at runtime
x = 42;            // ✅ reassign to anything
x.foo.bar.baz;     // ✅ no complaint — 'any' silences everything

// 'any' is CONTAGIOUS — it infects whatever you derive from it:
let len = x.length; // 'len' is 'any' too, so misuse stays unchecked

// Use 'any' only as a temporary bridge. The whole point of TypeScript
// is the checking that 'any' switches off.
// 'any' is the OPT-OUT type: it turns OFF all type checking for a value.
// In TS:  let x: any = ...  // you can do ANYTHING to x, no errors.
// At runtime there is no 'any' — it's just a normal value with no safety net.

let value = "hello"; // imagine: let value: any = "hello";
// With 'any', TS lets you call ANY method, even nonsense ones:
console.log(value.toUpperCase()); // HELLO  (this one is real)

// The danger: TS would ALSO allow value.notReal() at compile time...
// ...and it explodes at runtime, exactly where types should have helped:
try {
  value.notARealMethod();
} catch (e) {
  console.log("Crash: " + e.constructor.name); // Crash: TypeError
}

// 'any' is contagious — it spreads. Anything you derive from it is 'any' too:
let len = value.length; // typed 'any' in TS, so further misuse is unchecked
console.log(len); // 5

2. unknown: Safe by Default

unknown is what any should have been. You can assign any value to an unknown, but you cannot use it — no property access, no method calls — until you've narrowed it to a concrete type with typeof, instanceof, Array.isArray, or a custom type guard. This makes it the correct type for anything you don't control: parsed JSON, network responses, third-party callbacks.

// 'unknown' = "we don't know yet — prove it before you use it":
function describe(value: unknown): string {
  // value.length;            // ❌ Object is of type 'unknown'
  if (typeof value === "string") {
    return value.toUpperCase(); // ✅ now TS knows it's a string
  }
  if (typeof value === "number") {
    return (value * 2).toString(); // ✅ now it's a number
  }
  return String(value);
}

// 'unknown' is the RIGHT type for data you don't control:
const data: unknown = JSON.parse(input); // could be anything — narrow it!
// 'unknown' is the SAFE counterpart to 'any'. A value can be assigned to it,
// but you may NOT use it until you've NARROWED it to a real type.
// In TS:  let x: unknown = ...  // x.foo is an ERROR until you check what x is.

function describe(value) {
  // In TS, 'value: unknown' forces these checks before any use:
  if (typeof value === "string") {
    return "string of length " + value.length; // safe: proven string
  }
  if (typeof value === "number") {
    return "number, doubled = " + value * 2;    // safe: proven number
  }
  if (Array.isArray(value)) {
    return "array of " + value.length + " items";
  }
  return "something else: " + String(value);
}

console.log(describe("hi"));        // string of length 2
console.log(describe(21));          // number, doubled = 42
console.log(describe([1, 2, 3]));   // array of 3 items
console.log(describe(true));        // something else: true

// The point: with 'unknown' TS REFUSES value.length until you prove it's safe.

3. never: The Impossible Type

never represents a value that can never occur. It's the return type of functions that always throw or loop forever, and — most usefully — it powers exhaustiveness checking. By assigning the leftover value in a switch default to a never variable, you get a compile-time error the moment you add a new union member and forget to handle it.

// 'never' = a type that can never hold a value.

// 1) Functions that never return normally:
function fail(msg: string): never { throw new Error(msg); }
function loop(): never { while (true) {} }

// 2) Exhaustiveness checking — the famous compile-time safety net:
type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "square": return s.side ** 2;
    default:
      const _exhaustive: never = s; // ❌ ERRORS if you add a new Shape
      return _exhaustive;           // and forget to handle it here
  }
}
// 'never' is the type with NO possible values. It means "this can't happen".
// Two big uses: (1) a function that never returns, (2) exhaustiveness checks.
// In TS:  function fail(msg: string): never { throw new Error(msg); }

function fail(msg) {
  throw new Error(msg); // never returns normally -> return type 'never'
}

// Exhaustiveness: handle every member of a union; the 'default' should be
// unreachable. In TS we'd assign the leftover to a 'never' to PROVE that.
function area(shape) {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.r * shape.r;
    case "square": return shape.side * shape.side;
    default: {
      // In TS:  const _exhaustive: never = shape;  // errors if a case is missing
      return fail("Unknown shape: " + shape.kind);
    }
  }
}

console.log(area({ kind: "circle", r: 1 }).toFixed(2)); // 3.14
console.log(area({ kind: "square", side: 3 }));          // 9

try {
  area({ kind: "triangle" }); // hits the 'never' branch at runtime
} catch (e) {
  console.log("Caught: " + e.message); // Caught: Unknown shape: triangle
}

🎯 Your Turn

Here's the core unknown skill: narrow before you use. Fill in the two blanks marked ___, then run it.

// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// 'value' is 'unknown' in TS, so we must NARROW before using it.

function shout(value) {
  // 1) Prove it's a string with typeof before calling string methods:
  if (typeof value === ___) {   // 👉 the string "string"
    return value.toUpperCase() + "!";
  }
  // 2) Otherwise hand back a safe fallback:
  return ___;                   // 👉 the string "not a string"
}

console.log(shout("hi"));   // HI!
console.log(shout(42));     // not a string

// ✅ Expected output:
//    HI!
//    not a string

Common Errors (and the fix)

Pro Tips

📚 Keep learning

Mini-Challenge: Safe JSON Parsing

No blanks this time — just a brief and a starting outline. Build the safe parser yourself, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: safely parse untrusted JSON (the 'unknown' pattern).
// Real-world: JSON.parse returns 'any' — but the SAFE way is to treat the
// result as 'unknown' and narrow before trusting any field.
//
// Write safeParse(json) that:
//   1. Tries JSON.parse(json); if it throws, return "invalid JSON".
//   2. Checks the result is an object with a STRING 'name' field.
//   3. If so, return "Hello, <name>"; otherwise return "no name field".
//
// Test with: '{"name":"Ada"}', '{"age":3}', and 'not json'.
//
// ✅ Expected output:
//    Hello, Ada
//    no name field
//    invalid JSON

// your code here

🎉 Lesson Complete

Practice quiz

What does typing a value as any do to type checking?

  • Makes the value read-only
  • Forces a narrowing check before use
  • Turns OFF type checking for that value
  • Restricts it to objects

Answer: Turns OFF type checking for that value. any tells TypeScript to stop checking - you can call any method or reassign it freely, even operations that crash at runtime.

Why is any described as contagious?

  • Anything you derive from an any value is also any
  • It throws errors that spread across files
  • It automatically converts other types to any
  • It disables the whole project's type checking

Answer: Anything you derive from an any value is also any. Values derived from an any (like let len = x.length) are themselves any, so a single any quietly disables safety everywhere it spreads.

What must you do before using a value typed unknown?

  • Cast it to any first
  • Nothing - unknown behaves like any
  • Wrap it in a Promise
  • Narrow it to a concrete type with a check

Answer: Narrow it to a concrete type with a check. unknown accepts any value but blocks every operation until you narrow it with typeof, instanceof, Array.isArray, or a type guard.

Which type is the right choice for untrusted data like parsed JSON?

  • any
  • unknown
  • never
  • void

Answer: unknown. unknown keeps the safety net for data you don't control - you must prove its shape before trusting any field.

What is the never type?

  • The empty type - no value can ever have it
  • A value that is always undefined
  • Another name for null
  • A type for empty arrays only

Answer: The empty type - no value can ever have it. never is the empty type: no value can ever be a never. It's the return type of functions that throw or loop forever.

What is the return type of a function that always throws an error?

  • void
  • undefined
  • never
  • unknown

Answer: never. A function that never returns normally (it always throws or loops) has return type never.

In a switch, what does assigning the leftover value to a never variable in the default branch achieve?

  • It speeds up the switch at runtime
  • An exhaustiveness check - it errors if a case is unhandled
  • It converts the value to a string
  • It silences all type errors

Answer: An exhaustiveness check - it errors if a case is unhandled. If you add a new union member and forget a case, the value is no longer assignable to never, so TypeScript flags the gap.

Inside if (typeof value === "string") { ... }, what is value's type in that block?

  • unknown
  • any
  • never
  • string

Answer: string. Control-flow narrowing makes TypeScript treat value as string inside the branch, so string operations become allowed.

Which compiler flag helps keep accidental any out of your code?

  • strictNullChecks
  • noImplicitAny
  • noUnusedLocals
  • skipLibCheck

Answer: noImplicitAny. noImplicitAny makes TypeScript error when it would otherwise silently fall back to any, flushing out accidental any.

When you write an empty array literal, what element type can TypeScript infer in some positions?

  • any[]
  • unknown[]
  • never[]
  • object[]

Answer: never[]. An empty array can infer as never[] in some positions; annotate it (const xs: string[] = []) to avoid confusing 'not assignable to never' errors.

Continue this course

Frequently asked questions

What's the practical difference between any and unknown?

Both can hold a value of any type, but they behave oppositely when you USE the value. With 'any', TypeScript lets you do anything — call methods, access properties, reassign — with zero checks, so mistakes slip through to runtime. With 'unknown', TypeScript blocks every operation until you narrow the value (via typeof, instanceof, a type guard, etc.). 'unknown' gives you the same flexibility on input while keeping the safety on output.

When should I actually use 'any'?

Almost never in new code. Legitimate cases are temporary: migrating a large JavaScript file to TypeScript gradually, or quickly stubbing a third-party library with no types. Even then, prefer 'unknown' and narrow, or write a proper type. Many teams enable the 'noImplicitAny' compiler flag specifically to flush out accidental 'any'.

What does the 'never' type actually mean?

'never' is the empty type — no value can ever have it. It's the return type of functions that never finish normally (they throw or loop forever), and it appears in exhaustiveness checks: if you assign a union value to a 'never' variable in a switch's default branch, the code only compiles when every case is already handled. Add a new union member and forget a case, and TypeScript flags the 'never' assignment for you.

Why does narrowing 'unknown' make the error go away?

Inside a block like if (typeof value === 'string'), TypeScript performs control-flow narrowing: it knows that on this branch the value must be a string, so it temporarily treats it as one and allows string operations. The 'unknown' type forces you into these guarded branches, which is exactly what makes it safe — you can only touch the value where its type has been proven.