Comparison

TypeScript any vs unknown vs never: When to Use Each

Compare TypeScript's any, unknown, and never types with real examples, a comparison table, ESLint rules, and guidance on when to use each safely.

TypeScript any vs unknown vs never: When to Use Each

TypeScript ships three special types that beginners routinely confuse: any, unknown, and never. They sit at opposite corners of the type system, and picking the wrong one either disables type safety or blocks compilation. Here is a practical comparison.

Quick comparison

TypePositionAssign TO itAssign FROM itUse case
anyOpt-outAnythingAnything (unsafe)Escape hatch only
unknownTop typeAnythingNothing until narrowedUntrusted input
neverBottom typeNothingAnything (unreachable)Exhaustiveness, throws

any: the opt-out (considered harmful)

Using any tells the compiler to stop checking. Anything goes in, and anything comes out — including method calls on undefined. It silently disables the safety you installed TypeScript for.

let x: any = JSON.parse(input);
x.foo.bar.baz();      // compiles, crashes at runtime
const n: number = x;  // compiles, n might be a string

Enable "noImplicitAny": true in tsconfig.json so implicit anys (missing parameter types, untyped callbacks) are flagged. Add the ESLint rule @typescript-eslint/no-explicit-any to catch the explicit ones too. Reserve any for one-off interop with legacy JavaScript where the correct type is genuinely unknowable.

unknown: the safe top type

unknown accepts any value (like any) but forbids using it until you prove what it is. It is the correct type for JSON, catch clauses, and any external input.

function parseUser(raw: string): User {
  const data: unknown = JSON.parse(raw);

  if (
    typeof data === 'object' && data !== null &&
    'id' in data && typeof (data as any).id === 'number' &&
    'name' in data && typeof (data as any).name === 'string'
  ) {
    return data as User;
  }
  throw new Error('Invalid user payload');
}

Better: pair unknown with a proper type guard so the narrowing is reusable.

function isUser(x: unknown): x is User {
  return typeof x === 'object' && x !== null
    && typeof (x as User).id === 'number'
    && typeof (x as User).name === 'string';
}

const data: unknown = JSON.parse(raw);
if (isUser(data)) data.name.toUpperCase(); // safe

Also set "useUnknownInCatchVariables": true (default under strict) so caught errors are unknown, not any.

never: the bottom type

never is inhabited by no value. It appears in three places:

  • Functions that never return normally (throw or infinite loop).
  • Impossible branches after full narrowing.
  • Empty unions and empty intersections.
function fail(msg: string): never {
  throw new Error(msg);
}

Its killer feature is exhaustive switch checking. Assign the discriminant to a never-typed variable in the default branch, and the compiler errors the day you add a new case and forget to handle it.

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;
      throw new Error(`Unhandled: ${_exhaustive}`);
    }
  }
}

Add a { kind: 'triangle' } variant and the const _exhaustive: never = s line stops compiling until you handle it. This is the single most valuable idiom never enables.

Decision guide

  • Input from JSON, fetch, postMessage, or a plugin API — use unknown and narrow with a type guard or a validator like Zod.
  • Caught errors — leave them as unknown and check with instanceof Error.
  • Discriminated-union switch — assign the discriminant to never in the default.
  • Helpers that always throw — annotate the return type as never so callers get proper narrowing after them.
  • Reaching for any — stop and ask whether unknown, a generic, or a proper type would work. If you truly need it, add // eslint-disable-next-line @typescript-eslint/no-explicit-any with a comment explaining why.

Enforce the discipline in tsconfig.json with "strict": true, and in ESLint with @typescript-eslint/no-explicit-any, no-unsafe-assignment, no-unsafe-return, and switch-exhaustiveness-check. Together these rules push you toward unknown at the boundaries and never at the dead ends — where they belong.

typescript any typescript unknown typescript never top type bottom type exhaustive switch type narrowing no-explicit-any

Explore 300+ Free Tools

Utilko has tools for developers, writers, designers, students, and everyday users — all free, all browser-based.