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
| Type | Position | Assign TO it | Assign FROM it | Use case |
|---|---|---|---|---|
any | Opt-out | Anything | Anything (unsafe) | Escape hatch only |
unknown | Top type | Anything | Nothing until narrowed | Untrusted input |
never | Bottom type | Nothing | Anything (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 — useunknownand narrow with a type guard or a validator like Zod. - Caught errors — leave them as
unknownand check withinstanceof Error. - Discriminated-union switch — assign the discriminant to
neverin thedefault. - Helpers that always throw — annotate the return type as
neverso callers get proper narrowing after them. - Reaching for
any— stop and ask whetherunknown, a generic, or a proper type would work. If you truly need it, add// eslint-disable-next-line @typescript-eslint/no-explicit-anywith 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.