TypeScript any vs unknown: What's the Difference?
Both any and unknown can hold any value, but they behave oppositely for safety. any turns type checking off; unknown keeps it on and forces you to narrow before you use the value. The rule of thumb: prefer unknown.
Definition
In TypeScript, any and unknown are both top types that can hold any value, but they are opposites for safety. any disables type checking, so any operation is allowed. unknown keeps checking on and forbids every operation until you narrow the value to a specific type. Prefer unknown.
Start with what they share. Both any and unknown are top types: a variable of either type will accept a value of any other type. You can assign a string, a number, an object, or null to either one and TypeScript is perfectly happy. That is where the similarity ends. The difference is entirely about what happens next — what TypeScript will let you do with the value once it is sitting in that variable.
// Both accept anything on the way IN
let a: any = 42;
a = "hello";
a = { id: 1 }; // fine
let u: unknown = 42;
u = "hello";
u = { id: 1 }; // also fine
// The difference is what you can do NEXT
a.toUpperCase(); // OK — any disables all checks (may crash at runtime)
u.toUpperCase(); // ❌ Error: 'u' is of type 'unknown'any is TypeScript's off switch for type checking. When a value is any, the compiler stops reasoning about it: you can call any method, read any property, index it, or pass it anywhere, and you will get zero errors — even for operations that will throw at runtime. The TypeScript Handbook is blunt that any exists as an escape hatch for when you do not want a value to cause type errors, and warns that using it defeats the point of having a type checker at all.
Why any is contagious
The real danger of any is not the one value you knowingly opt out on — it is how far the opt-out spreads. Because you can assign an any to any typed variable without a complaint, a single any silently disables checking on everything it touches downstream. It flows into typed variables, through function returns, and across module boundaries, quietly widening the hole. People call it contagious for exactly this reason.
function getConfig(): any { // one any at the source...
return JSON.parse(rawConfig);
}
const port: number = getConfig().port; // no error — any flows into a number
const host: string = getConfig().hsot; // typo, still no error
port.toFixed(); // 'port' is really a string? undefined? any hid it
host.trim(); // crashes at runtime — the compiler never checkedunknown: safe by forcing you to narrow
unknown, added as a top type in TypeScript 3.0, is the type-safe counterpart. It accepts any value on the way in, just like any — but it lets you do nothing with that value until you have proven what it is. You cannot read a property, call a method, or assign it to a typed variable while it is still unknown. To use it, you must narrow it: convince the compiler, through a runtime check, that the value is a more specific type. Inside the branch where the check passes, TypeScript treats the value as that proven type.
function render(value: unknown) {
// typeof — narrow a primitive
if (typeof value === "string") {
return value.toUpperCase(); // ✓ value is string here
}
// instanceof — narrow a class
if (value instanceof Error) {
return value.message; // ✓ value is Error here
}
// custom type guard — narrow an object shape
if (isUser(value)) {
return value.name; // ✓ value is User here
}
}
// A type guard returns a `value is T` predicate
function isUser(v: unknown): v is { name: string } {
return typeof v === "object" && v !== null && "name" in v;
}The four everyday narrowing tools are all in the Handbook's Narrowing chapter: typeof for primitives, instanceof for classes, custom type-guard functions that return a value is T predicate, and — most robust for external data — a runtime schema validator such as Zod, which parses an unknown and hands back a fully typed result (or throws). The point is identical every time: unknown makes the check mandatory. You cannot forget it, because the code will not compile until it is there.
import { z } from "zod";
const User = z.object({ id: z.number(), name: z.string() });
async function loadUser(): Promise<{ id: number; name: string }> {
const res = await fetch("/api/user");
const data: unknown = await res.json(); // json() returns unknown-shaped data
return User.parse(data); // narrows unknown → typed, or throws
}The classic use case: genuinely unknown values
unknown is the correct type for any value whose shape you cannot know at compile time. The compiler literally cannot see across those boundaries, so being honest about it — and being forced to validate — is exactly what you want. The canonical cases are: the result of JSON.parse(), the body of a fetch response, data from a message channel or postMessage, values coming from third-party code without types, and errors in a catch clause.
try {
doWork();
} catch (e) { // e: unknown
// e.message // ❌ Error: 'e' is of type 'unknown'
if (e instanceof Error) {
console.error(e.message); // ✓ narrowed to Error
} else {
console.error(String(e)); // handle non-Error throws
}
}When any is (rarely) pragmatic
any is not forbidden — it is a tool with a narrow, honest job. It is defensible when you are migrating a large untyped JavaScript codebase one file at a time, when a third-party .d.ts is wrong and fighting it costs more than the risk, or in throwaway prototype code. The discipline when you do use it is containment: keep the any in the smallest possible local scope, cast it back to a real type at the boundary immediately, and never let it into a shared or exported signature where it can spread. If the value is merely unknown rather than ignored, reach for unknown — you get the same flexibility with none of the leakage.
How both differ from never
It helps to place these against never, the bottom type — the mirror image of the top types. Where unknown is 'a value that could be anything,' never is 'a value that can be nothing': it is the type of a function that never returns (it throws or loops forever) and of the impossible branch in an exhaustive check. You can assign anything to unknown but nothing to never; conversely a never is assignable to every type, because a value that cannot exist vacuously satisfies any constraint. So the three form a spectrum: never holds no values, a normal type holds its own values, and any/unknown hold all values — differing only in whether the compiler keeps watching.
| Feature | Behaviour | any | unknown |
|---|---|---|---|
| Type checking on the value | — | Turned OFF | Stays ON |
| Can you use it without narrowing? | — | ✓ | — |
| Spreads / contagious? | — | ✓ | — |
| Safety | — | Unsafe (escape hatch) | Type-safe |
| Typical use | — | Migration, wrong 3rd-party types, prototypes — contained | JSON.parse, API responses, catch (e), untyped data |
If you want the wider context, our guide on common TypeScript errors shows how the mistakes any hides tend to resurface, and TypeScript vs JavaScript covers why static checking earns its keep in the first place. The through-line: any spends the safety TypeScript gives you; unknown keeps it.
And when an any does let a runtime bug slip through to a user, that is exactly the failure BugMojo is built to catch. Its browser extension records the session with an rrweb replay, the console error, and the network response that carried the bad shape — so instead of a bare cannot read properties of undefined, your AI agent sees the exact API payload the any failed to check, and can trace it back to the boundary where type safety stopped.
BugMojo captures the failing session — rrweb replay, console, and the network response behind the error — then hands the whole bundle to Claude Code or Cursor over MCP, so your agent sees the real shape your types missed.
Install the extensionFrequently asked questions
Frequently asked questions
Sources
- The any type — TypeScript Handbook, 'Everyday Types' — TypeScript (2026)
- Narrowing — type guards, typeof, instanceof and custom guards (TypeScript Handbook) — TypeScript (2026)
- useUnknownInCatchVariables — catch clause variables are typed unknown (tsconfig reference) — TypeScript (2026)
- TypeScript 3.0 release notes — the new unknown top type — TypeScript (2026)
Get bug-tracking insights, weekly.
Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.

