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.
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
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.
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
}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
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
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)
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 }
}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
// 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
// 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:
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;
}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:
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:
// 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:
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 dangerousas) 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
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[] | nullPattern 2: Error Message Extraction
// 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