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 any and unknown accept a value of any type on assignment — the compiler never rejects let x: any = someValue or let 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. For unknown, the answer is always no, until the compiler can prove otherwise.
  • Narrowing: control-flow analysis is how you “prove otherwise.” A typeof check, an instanceof check, an in check, a user-defined type guard (value is Foo), or a type assertion (value as Foo) all narrow an unknown down to something usable within that branch or from that point forward.
  • Contagion: any is “contagious” — once a value is any, anything derived from it (a property access, a function return value) is usually also any, silently spreading the loss of type safety through your code. unknown does 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: any and let y: unknown is identical — just let x; and let y;. All the safety unknown buys you is enforced purely by tsc at 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 unknown over any whenever a value’s type is genuinely not known ahead of time — API responses, JSON.parse results, values from third-party libraries without types, and catch-clause errors.
  • Treat any as 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 unknown with a real runtime check (typeof, instanceof, a type guard function) rather than an unchecked as assertion, 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 with strict) so the compiler flags places where TypeScript would otherwise silently infer any, such as untyped function parameters.
  • In a catch block, treat the caught value as unknown (TypeScript does this by default under strict/useUnknownInCatchVariables) and narrow it before reading .message or similar properties.
  • Avoid letting any spread 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): string that returns "a string: ...", "a number: ...", or "something else" depending on the runtime type of value, using typeof narrowing. Test it with a string, a number, and a boolean.
  • Exercise 2: Rewrite a function that currently takes value: any and calls value.toFixed(2) so that it instead takes value: 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 type unknown received from JSON.parse.

Summary

  • any completely disables type checking for a value — anything goes, and mistakes only surface at runtime.
  • unknown can also hold any value, but the compiler blocks all operations on it until you narrow it to a specific type.
  • Narrowing unknown is done with typeof, instanceof, in checks, or user-defined type guards (value is Foo).
  • Both types are erased at compile time — the safety unknown provides exists only in tsc, not at runtime.
  • Prefer unknown by default for values of uncertain type; reserve any for genuine escape-hatch situations.