TypeScript any vs unknown
TypeScript gives you two ways to say “I don’t know the type of this value yet”: any and unknown. Both let you accept values without pinning down their shape up front, but they behave very differently once the compiler gets involved. any turns off type checking entirely for a value, while unknown keeps checking active and forces you to prove what a value is before you use it. Understanding the difference is one of the fastest ways to write TypeScript that is both flexible and actually safe.
Overview / How it works
TypeScript’s type checker exists to catch mistakes before your code runs — calling a method that doesn’t exist, passing a string where a number is expected, and so on. any is a deliberate escape hatch: when a value has type any, the compiler stops checking it. You can call any method on it, access any property, assign it to any other variable, and pass it to any function parameter, and TypeScript will never complain. This is useful for gradually migrating JavaScript code to TypeScript, or for genuinely dynamic values, but it comes at a cost: any bug involving an any-typed value is invisible to the compiler and only shows up at runtime.
unknown, introduced in TypeScript 3.0, was designed as a type-safe alternative. Like any, a variable of type unknown can hold a value of any type — a string, a number, an object, anything. The difference is what you’re allowed to do with it. The compiler will not let you call methods on an unknown value, access its properties, or use it as a more specific type until you’ve proven — through a type guard, an assertion, or a runtime check — what it actually is. In other words, any disables the type system, while unknown keeps the type system on and simply says “this could be anything, so earn the right to use it.”
Both types are purely compile-time constructs. Like every TypeScript type, any and unknown are erased when your code is compiled to JavaScript — there is no runtime trace of them at all. The safety unknown provides is entirely a property of the type checker catching mistakes before your code ever runs; at runtime, a value declared as unknown is just a plain JavaScript value like any other.
Syntax
let a: any;
let u: unknown;
Both are declared exactly like any other type annotation. The difference shows up in assignability — what you can assign to them and, more importantly, what you can do with them afterward.
| Operation | any |
unknown |
|---|---|---|
| Assign any value to it | Allowed | Allowed |
Assign it to a variable of a specific type (e.g. string) without narrowing |
Allowed | Error |
| Access a property or call a method on it directly | Allowed | Error |
Assign it to another any or unknown variable |
Allowed | Allowed |
Use after narrowing with typeof, instanceof, or a type guard |
N/A (already unrestricted) | Allowed, as the narrowed type |
Examples
Example 1: any bypasses type checking entirely
function processValue(value: any) {
console.log(value.toUpperCase());
}
processValue("hello");
try {
processValue(42);
} catch (error) {
console.log("Runtime error:", (error as Error).message);
}
Output:
HELLO
Runtime error: value.toUpperCase is not a function
Because value is typed any, TypeScript happily lets us call .toUpperCase() on it without complaint — even though that method doesn’t exist on numbers. The mistake only surfaces when the code actually runs and throws. This is exactly the risk any introduces: the compiler gives up checking, so bugs slip through to runtime.
Example 2: unknown forces you to narrow first
function processValue(value: unknown) {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else if (typeof value === "number") {
console.log(value.toFixed(2));
} else {
console.log("Unsupported type:", typeof value);
}
}
processValue("hello");
processValue(42);
processValue(true);
Output:
HELLO
42.00
Unsupported type: boolean
Here, value is unknown, so TypeScript refuses to let us call .toUpperCase() or .toFixed() until we’ve narrowed the type with a typeof check. Inside each branch, TypeScript knows the exact type — string in the first branch, number in the second — so those calls are fully type-checked and safe. Unlike Example 1, there’s no way to accidentally call the wrong method here; the compiler would reject it before the code even runs.
Example 3: a realistic case — safely parsing untrusted JSON
interface User {
id: number;
name: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
typeof (value as User).id === "number" &&
typeof (value as User).name === "string"
);
}
function parseUser(json: string): User {
const data: unknown = JSON.parse(json);
if (!isUser(data)) {
throw new Error("Invalid user data");
}
return data;
}
const user = parseUser('{"id": 1, "name": "Ada"}');
console.log(`User #${user.id}: ${user.name}`);
try {
parseUser('{"id": "not-a-number"}');
} catch (error) {
console.log("Failed to parse:", (error as Error).message);
}
Output:
User #1: Ada
Failed to parse: Invalid user data
JSON.parse actually returns any in TypeScript’s standard library — but by immediately annotating the result as unknown, we force ourselves to validate the shape of the data with a user-defined type guard (isUser, which returns a special value is User type) before treating it as a User. This is the pattern you’ll use constantly for API responses, form input, and anything else arriving from outside your program’s control.
How it works step by step / Under the hood
- Assignment checks: both
anyandunknownaccept a value of any type on assignment — the compiler never rejectslet x: any = someValueorlet y: unknown = someValue. - Usage checks: this is where they diverge. For every operation on a value — property access, method calls, arithmetic, spreading, calling it as a function — the compiler asks “does this type support that operation?” For
any, the answer is always yes, unconditionally. Forunknown, the answer is always no, until the compiler can prove otherwise. - Narrowing: control-flow analysis is how you “prove otherwise.” A
typeofcheck, aninstanceofcheck, anincheck, a user-defined type guard (value is Foo), or a type assertion (value as Foo) all narrow anunknowndown to something usable within that branch or from that point forward. - Contagion:
anyis “contagious” — once a value isany, anything derived from it (a property access, a function return value) is usually alsoany, silently spreading the loss of type safety through your code.unknowndoes not spread this way; you must explicitly narrow it at each point of use. - Erasure at runtime: after compilation, none of this exists. The emitted JavaScript for
let x: anyandlet y: unknownis identical — justlet x;andlet y;. All the safetyunknownbuys you is enforced purely bytscat compile time; it has zero runtime cost and zero runtime presence.
Common Mistakes
Mistake 1: Reaching for any “just to make the error go away”
function getLength(value: any) {
return value.length;
}
console.log(getLength("hello"));
console.log(getLength(42));
Output:
5
undefined
This compiles with zero errors — that’s exactly the problem. value.length is accepted for any input because any disables checking, so a number silently produces undefined instead of a compiler error warning you that numbers don’t have a .length property. Bugs like this hide until they cause a real failure somewhere downstream.
Corrected, using unknown and a narrowing check:
function getLength(value: unknown): number {
if (typeof value === "string" || Array.isArray(value)) {
return value.length;
}
throw new Error("Value has no length");
}
console.log(getLength("hello"));
console.log(getLength([1, 2, 3]));
Output:
5
3
Now passing a number would be caught immediately — either as a thrown error at runtime, or, more usefully, TypeScript prevents you from writing code that assumes .length exists without checking first.
Mistake 2: Using unknown like it’s any
function getLength(value: unknown) {
return value.length;
}
This does not compile. tsc reports: Object is of type 'unknown'. This is unknown doing its job — it refuses to let you access .length until you’ve narrowed the type, which is exactly what the corrected version of Mistake 1 does. If you find yourself adding type assertions (as SomeType) everywhere just to satisfy unknown without any real check behind them, you’ve lost most of the safety benefit — an assertion just tells the compiler to trust you, with no runtime verification.
Best Practices
- Default to
unknownoveranywhenever a value’s type is genuinely not known ahead of time — API responses,JSON.parseresults, values from third-party libraries without types, and catch-clause errors. - Treat
anyas a last resort for truly dynamic, legacy, or migration-in-progress code — not a convenient way to silence an error you don’t understand. - Always narrow
unknownwith a real runtime check (typeof,instanceof, a type guard function) rather than an uncheckedasassertion, which provides no actual safety. - Write reusable type guard functions (
function isFoo(x: unknown): x is Foo) for shapes you validate often, such as API response payloads. - Enable
noImplicitAny(on by default withstrict) so the compiler flags places where TypeScript would otherwise silently inferany, such as untyped function parameters. - In a
catchblock, treat the caught value asunknown(TypeScript does this by default understrict/useUnknownInCatchVariables) and narrow it before reading.messageor similar properties. - Avoid letting
anyspread through your codebase — a single untyped boundary can silently erase type safety in every function that touches its output.
Practice Exercises
- Exercise 1: Write a function
describe(value: unknown): stringthat returns"a string: ...","a number: ...", or"something else"depending on the runtime type ofvalue, usingtypeofnarrowing. Test it with a string, a number, and a boolean. - Exercise 2: Rewrite a function that currently takes
value: anyand callsvalue.toFixed(2)so that it instead takesvalue: unknown, checks that it’s a number before calling.toFixed, and throws a descriptive error otherwise. - Exercise 3: Write a type guard
isStringArray(value: unknown): value is string[]that checks whether a value is an array where every element is a string. Use it to safely process a value of typeunknownreceived fromJSON.parse.
Summary
anycompletely disables type checking for a value — anything goes, and mistakes only surface at runtime.unknowncan also hold any value, but the compiler blocks all operations on it until you narrow it to a specific type.- Narrowing
unknownis done withtypeof,instanceof,inchecks, or user-defined type guards (value is Foo). - Both types are erased at compile time — the safety
unknownprovides exists only intsc, not at runtime. - Prefer
unknownby default for values of uncertain type; reserveanyfor genuine escape-hatch situations.
