TypeScriptany vs unknown

any vs unknown

Two types in TypeScript accept values of any shape: any and unknown. They look similar on the surface but behave very differently. Understanding the distinction is one of the most important concepts in TypeScript — and choosing between them determines whether your code stays type-safe or quietly opts out of it.

The Problem They Both Solve

Sometimes you genuinely don't know the type of a value at compile time. The classic examples:

  • Data fetched from an API (the server could return anything)
  • User input from a form
  • Values parsed from JSON
  • Arguments passed to a generic error handler
  • Data read from localStorage

In all these cases, the value's type cannot be known statically. TypeScript provides two solutions: the permissive any and the safe unknown.

any: The Escape Hatch

any tells TypeScript: "I opt out of type checking for this value. Trust me." Once a value is any, you can do anything with it — call it, index into it, assign it anywhere — and TypeScript will not complain.

TS
let value: any = fetchFromAPI();

// TypeScript allows all of these without complaint
value.foo.bar.baz;           // accessing arbitrary properties
value();                     // calling it as a function
value[0][1][2];              // indexing arbitrarily
const n: number = value;    // assigning to any other type
const s: string = value;    // no error
Warning
Every one of those operations could throw at runtime. TypeScript does not check them because any disables type checking entirely for that value. You get no protection, no autocomplete, and no error messages — just trust.
unknown: The Safe Alternative

unknown is the type-safe counterpart. Like any, it accepts values of any type. Unlike any, TypeScript refuses to let you use an unknown value until you prove (via a type check) what it actually is.

TS
let value: unknown = fetchFromAPI();

// TypeScript blocks all of these
value.foo;           // Error: Object is of type 'unknown'
value();             // Error: Object is of type 'unknown'
const n: number = value;  // Error: Type 'unknown' is not assignable to type 'number'

// You must narrow the type first
if (typeof value === 'string') {
  console.log(value.toUpperCase());  // OK — TypeScript knows it is string here
}

if (typeof value === 'number') {
  console.log(value.toFixed(2));     // OK — TypeScript knows it is number here
}
Success
unknown forces you to be explicit about what you expect — exactly the discipline that makes code reliable.
Side-by-Side Comparison

Operation

any

unknown

Assign any value to it

Yes

Yes

Assign it to another typed variable

Yes (unsafe)

No — must narrow first

Access properties on it

Yes (unsafe)

No — must narrow first

Call it as a function

Yes (unsafe)

No — must narrow first

Use without type checking

Yes

No

Provides type safety

No

Yes

Works with type guards

Not needed

Required

Narrowing unknown Values

To use an unknown value you must narrow its type. TypeScript supports several narrowing techniques:

typeof Narrowing

TS
function processInput(input: unknown) {
  if (typeof input === 'string') {
    return input.toUpperCase();   // string
  }
  if (typeof input === 'number') {
    return input.toFixed(2);      // number
  }
  if (typeof input === 'boolean') {
    return input ? 'yes' : 'no';  // boolean
  }
  return 'unsupported type';
}
instanceof Narrowing

TS
function handleError(error: unknown): string {
  if (error instanceof Error) {
    return error.message;   // TypeScript knows: Error
  }
  if (typeof error === 'string') {
    return error;           // TypeScript knows: string
  }
  return 'An unknown error occurred';
}
Property Existence Check (in operator)

TS
function isUser(value: unknown): value is { name: string; email: string } {
  return (
    typeof value === 'object' &&
    value !== null &&
    'name' in value &&
    'email' in value &&
    typeof (value as any).name === 'string' &&
    typeof (value as any).email === 'string'
  );
}

const data: unknown = JSON.parse(apiResponse);

if (isUser(data)) {
  console.log(data.name);   // OK — narrowed to { name: string; email: string }
}
Tip
The pattern of writing a function that returns value is SomeType is called a type predicate (or type guard function). It lets you encapsulate complex narrowing logic into a reusable function.
When to Use any

any is occasionally the right tool. Here are the legitimate use cases:

  • Migrating a large JavaScript codebase to TypeScript incrementally — use any temporarily while adding types file by file

  • Third-party libraries with no type definitions and no @types/* package available

  • Complex dynamic code where the type system cannot model what you are doing (rare)

  • Test utilities that intentionally bypass type checking

TS
// Legitimate use: migrating a legacy JS function
// Before: was just JS, no types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function legacyTransform(data: any): any {
  // TODO: type this properly
  return data.map((x: any) => x.value);
}

// Legitimate use: complex dynamic proxy
function createProxy<T extends object>(target: T): T {
  return new Proxy(target, {
    get(obj: any, prop: string) {  // prop access on handler is dynamic
      return obj[prop];
    },
  });
}
When to Use unknown

Reach for unknown in any situation where the type is truly not known at compile time but you want to retain safety:

  • API responses — validate the shape before using the data

  • Error handler parameters — catch (e: unknown) is the safest signature

  • JSON.parse() return values — the parsed value could be anything

  • Values from localStorage or sessionStorage

  • Third-party SDK callbacks where the payload type is documented but not typed

TS
// Best practice: catch clause
try {
  await riskyOperation();
} catch (error: unknown) {
  // TypeScript 4.0+ allows : unknown here
  if (error instanceof Error) {
    console.error('Known error:', error.message);
  } else {
    console.error('Unknown error:', String(error));
  }
}

// Best practice: API response
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data: unknown = await response.json();

  // Validate before trusting
  if (!isValidUser(data)) {
    throw new Error('Invalid user data from API');
  }
  return data;  // TypeScript knows it is User here
}
Validating unknown with a Schema Library

Writing manual type guards for complex objects is tedious. Libraries like Zod or Valibot let you declare a schema and get both a runtime validator and TypeScript types in one go:

TS
import { z } from 'zod';

// Define schema — also generates the TypeScript type
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0).max(150).optional(),
});

type User = z.infer<typeof UserSchema>;
// type User = { id: number; name: string; email: string; age?: number | undefined }

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const raw: unknown = await response.json();

  // Zod validates and narrows in one call
  const user = UserSchema.parse(raw);  // throws if invalid, returns User if valid
  return user;
}
Tip
Use schema.safeParse(data) instead of schema.parse(data) when you want to handle validation errors gracefully without a try/catch block. It returns { success: true, data: T } | { success: false, error: ZodError }.
any Spreading: The Silent Danger

any is contagious — once a value is any, operations on it also produce any. This "any spreading" erodes your type safety silently:

TS
const data: any = fetchRawData();

// Every derived value is also 'any' — type safety is gone
const users = data.users;          // any
const firstName = users[0].name;   // any
const upper = firstName.toUpperCase(); // any — but could crash at runtime

// With unknown, the spreading stops at the boundary
const safeData: unknown = fetchRawData();
const safeUsers = (safeData as any).users;  // explicit cast — visible in code review
// At this point, safeUsers is any — but at least the cast was intentional
noImplicitAny: Prevent Accidental any

The compiler option noImplicitAny (part of strict: true) prevents TypeScript from silently inferring any for untyped parameters. With it enabled, you must be explicit:

TS
// noImplicitAny: true

// Error: Parameter 'x' implicitly has an 'any' type
function double(x) {
  return x * 2;
}

// Fix: annotate the parameter
function double(x: number): number {
  return x * 2;
}

// Or use unknown if you don't know the type
function log(value: unknown): void {
  console.log(value);
}
Type Assertions vs unknown Narrowing

Both type assertions (as SomeType) and narrowing work with unknown. They are not the same thing:

TS
const raw: unknown = getExternalData();

// Type assertion — you are taking responsibility, TypeScript trusts you
const user = raw as User;  // no runtime check — could crash

// Narrowing — TypeScript verifies the type at runtime via your check
if (isUser(raw)) {
  const user = raw;  // TypeScript knows it is User — runtime-verified
}

// Double assertion — a red flag (you're forcing an impossible conversion)
const n = raw as unknown as number;  // compiles, but dangerous
Warning
Use type assertions (as) only when you can guarantee the type from context that TypeScript cannot see. Always prefer narrowing (type guards) when you are dealing with runtime data like API responses or parsed JSON.
Practical Patterns
Pattern 1: Safe JSON Parse Wrapper

TS
function safeJsonParse<T>(
  json: string,
  validator: (value: unknown) => value is T,
): T | null {
  try {
    const parsed: unknown = JSON.parse(json);
    return validator(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

// Usage
function isStringArray(v: unknown): v is string[] {
  return Array.isArray(v) && v.every((x) => typeof x === 'string');
}

const tags = safeJsonParse('["ts","js","react"]', isStringArray);
// tags: string[] | null
Pattern 2: Error Message Extraction

TS
// The catch variable type in TypeScript 4.0+ is unknown
function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  if (typeof error === 'string') return error;
  if (
    typeof error === 'object' &&
    error !== null &&
    'message' in error &&
    typeof (error as Record<string, unknown>).message === 'string'
  ) {
    return (error as Record<string, unknown>).message as string;
  }
  return 'An unexpected error occurred';
}

// Usage
try {
  await fetchData();
} catch (e) {
  console.error(getErrorMessage(e));
}
Summary

Question

Answer

What does any do?

Disables all type checking for a value

What does unknown do?

Accepts any value but requires narrowing before use

When should I use any?

Rarely — migration, untyped third-party code, test utilities

When should I use unknown?

API responses, JSON.parse, catch clauses, localStorage

Is any safe?

No — it is an escape hatch, not a type

Is unknown safe?

Yes — it enforces explicit handling of all possible types

  • any disables TypeScript protection — the compiler will not catch errors involving it

  • unknown is the type-safe escape hatch — you must narrow before using the value

  • any is contagious — operations on any values produce more any values

  • unknown stops spreading — you must be explicit at every boundary

  • Use type predicates and schema libraries to validate and narrow unknown values

  • Enable noImplicitAny (via strict: true) to prevent accidental any inference

  • In catch clauses, use unknown instead of any for maximum safety