TypeScript unknown vs any: What's the Difference
TypeScript's unknown forces a type check before use; any opts out of type checking entirely. When to reach for each in real code.
TypeScript’s unknown and any both mean “this value could be anything,” but they behave almost oppositely once you try to use the value. any disables type checking on everything it touches, while unknown requires you to narrow the type before you can do anything with it. Reaching for any out of habit is one of the most common ways a TypeScript codebase quietly loses its safety guarantees.
What any actually does
any is an escape hatch. Once a value is typed any, TypeScript stops checking it — and, worse, stops checking anything derived from it. You can call methods that don’t exist, pass it where a completely different type is expected, and access arbitrary properties, all without a single compiler error:
function process(input: any) {
input.toUpperCase(); // no error, even if input is a number
input.whatever.deeply.nested; // still no error
const n: number = input; // no error, even if input is a string
}
The problem isn’t just that any is unchecked — it’s contagious. Any value assigned from an any-typed expression becomes any itself, silently widening the blast radius of a single loosely-typed value through the rest of the function.
What unknown actually does
unknown also accepts any value, but the compiler refuses to let you do anything with it until you’ve proven what it actually is:
function process(input: unknown) {
input.toUpperCase(); // compiler error: input is unknown
if (typeof input === "string") {
input.toUpperCase(); // fine — narrowed to string
}
}
You’re forced to narrow with typeof, instanceof, a custom type guard, or a schema-validation library before the value becomes usable. This is the entire point: unknown says “I don’t know the type yet,” while any says “don’t bother checking.”
Side by side
any | unknown | |
|---|---|---|
| Accepts any value | Yes | Yes |
| Requires narrowing before use | No | Yes |
| Property/method access without a check | Allowed | Compiler error |
| Assignable to other specific types directly | Yes | No — requires a narrowing check first |
| Propagates looseness to derived values | Yes | No |
| Best for | Legacy migration, deliberate opt-out | External input: API responses, JSON.parse, catch blocks |
Where each one shows up in practice
unknown is the correct type for anything arriving from outside your program’s control: the result of JSON.parse(), a fetch response body before validation, or a caught exception in a catch block (which TypeScript types as unknown by default in modern configurations, precisely to stop you from assuming its shape). In each case you don’t actually know the type yet, and unknown makes the compiler hold you to proving it before use.
try {
riskyOperation();
} catch (err: unknown) {
if (err instanceof Error) {
console.log(err.message); // safe — narrowed
}
}
any still has legitimate uses, but they’re narrower than most codebases treat them: incrementally migrating a JavaScript file where full typing isn’t worth the effort yet, or interfacing with a poorly-typed third-party library where writing an accurate type would take longer than the value it provides. Even then, it’s worth scoping the any as tightly as possible rather than letting it spread through a function’s return type.
Narrowing unknown without a runtime library
Type guards are the idiomatic way to narrow unknown into something usable:
function isUser(value: unknown): value is { id: string; name: string } {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
);
}
function greet(input: unknown) {
if (isUser(input)) {
console.log(`Hello, ${input.name}`); // narrowed
}
}
For anything more complex — nested objects, arrays of records, optional fields — a schema-validation library that both validates at runtime and infers the TypeScript type from the same schema is generally less error-prone than hand-written guards, since the check and the type can’t drift apart.
Why this matters for real bugs
The difference isn’t academic. any is how a null from an API response ends up three function calls deep before it crashes at runtime with no compiler warning along the way — the type system simply stopped watching the moment the value was labeled any. unknown converts that same class of bug into a compile-time error: you cannot call .toUpperCase() on an unknown without TypeScript stopping you first, which pushes the “what if this isn’t a string” question to the point where the data first enters your program instead of wherever it happens to blow up. If you’re just getting oriented with the language, see getting started with TypeScript for the fundamentals this builds on, and TypeScript generics and utility types for how to keep types precise elsewhere in a codebase. The satisfies operator solves an adjacent problem — validating a value’s shape without widening its inferred type.
The takeaway
any turns off type checking; unknown keeps it on but demands proof before letting you use the value. Default to unknown for anything whose shape you don’t control yet — API responses, parsed JSON, caught errors — and narrow it with a type guard before use. Reserve any for the rare cases where an accurate type genuinely isn’t worth writing, and keep its scope as small as possible so its looseness doesn’t spread.
Keep reading
Takina · · 4 min read TypeScript Abstract Classes, Explained
Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.
Takina · · 3 min read TypeScript's never Type, Explained
never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.
Takina · · 4 min read TypeScript readonly Modifiers Explained
TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.