TypeScript Unknown vs Any: Safe Unknown Handling
TypeScript exists to bring static typing to JavaScript's dynamic world, but what happens when you genuinely don't know a value's type? For years, developers reached for `any`, TypeScript's escape...
Key Insights
unknownforces type validation before use, catching errors at compile time thatanywould miss until runtime- Type narrowing with
typeof,instanceof, and custom type guards transformsunknowninto specific types safely - Use
unknownfor external data (API responses, JSON parsing) and reserveanyonly for migration scenarios or truly dynamic code
The Type Safety Spectrum
TypeScript exists to bring static typing to JavaScript’s dynamic world, but what happens when you genuinely don’t know a value’s type? For years, developers reached for any, TypeScript’s escape hatch that essentially says “trust me, I know what I’m doing.” The compiler would shrug and let you do whatever you wanted.
TypeScript 3.0 introduced unknown as a safer alternative. While both represent values of uncertain types, they sit at opposite ends of the type safety spectrum. Understanding the difference isn’t academic—it’s the difference between catching bugs during development and debugging production incidents at 2 AM.
Understanding any: The Type Safety Escape Hatch
The any type tells TypeScript to disable all type checking for a value. You can call methods, access properties, perform arithmetic, or assign it anywhere—the compiler won’t stop you. This flexibility comes at a steep cost: you’ve opted out of the very protection TypeScript provides.
function processData(data: any) {
// All of these compile without errors
const result = data.toUpperCase();
const sum = data + 100;
const user = data.name.firstName;
data.nonExistentMethod();
return result;
}
// Compiles fine, crashes at runtime
processData(42); // TypeError: data.toUpperCase is not a function
This code compiles successfully despite being riddled with potential runtime errors. If data is a number, calling toUpperCase() crashes. If it lacks a name property, accessing name.firstName throws. The any type gives you a false sense of security—your code compiles, but you’ve simply deferred error checking to runtime.
The problem compounds in larger codebases. Once a value is typed as any, it pollutes everything it touches. Assign it to a variable, pass it to a function, or return it—the any spreads, creating expanding blind spots in your type system.
Understanding unknown: Type-Safe Alternative
The unknown type represents the same concept—a value whose type we don’t know—but enforces safety. You can assign any value to unknown, but you cannot perform operations on it without first narrowing its type. The compiler forces you to prove you know what you’re dealing with.
function processData(data: unknown) {
// All of these produce compile-time errors
const result = data.toUpperCase(); // Error: Object is of type 'unknown'
const sum = data + 100; // Error: Object is of type 'unknown'
const user = data.name; // Error: Object is of type 'unknown'
// You must narrow the type first
if (typeof data === 'string') {
const result = data.toUpperCase(); // OK: data is string here
return result;
}
return 'Invalid data';
}
processData(42); // Returns 'Invalid data' safely
processData('hello'); // Returns 'HELLO'
The errors aren’t TypeScript being difficult—they’re TypeScript preventing bugs. You’re forced to handle the uncertainty explicitly, which means your code documents its assumptions and handles edge cases.
Type Narrowing Techniques for unknown
Type narrowing is how you convert unknown into something usable. TypeScript’s control flow analysis tracks your type checks and refines types accordingly.
Basic Type Guards
The typeof operator works for primitives:
function formatValue(value: unknown): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
if (typeof value === 'number') {
return value.toFixed(2);
}
if (typeof value === 'boolean') {
return value ? 'Yes' : 'No';
}
return 'Unknown type';
}
For objects and class instances, use instanceof:
class User {
constructor(public name: string) {}
}
function greet(value: unknown): string {
if (value instanceof User) {
return `Hello, ${value.name}`;
}
if (value instanceof Error) {
return `Error: ${value.message}`;
}
return 'Hello, stranger';
}
Custom Type Guards
For complex types, create type guard functions with type predicates:
interface User {
id: number;
name: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value &&
typeof (value as User).id === 'number' &&
typeof (value as User).name === 'string' &&
typeof (value as User).email === 'string'
);
}
function processUser(data: unknown) {
if (isUser(data)) {
// data is now typed as User
console.log(`User: ${data.name} (${data.email})`);
return data.id;
}
throw new Error('Invalid user data');
}
The value is User return type is a type predicate that tells TypeScript “if this function returns true, treat the value as User in the calling scope.”
Property Checking with in
The in operator checks for property existence and narrows types:
type Success = { success: true; data: string };
type Failure = { success: false; error: string };
type Result = Success | Failure;
function handleResult(result: unknown) {
if (
typeof result === 'object' &&
result !== null &&
'success' in result
) {
if (result.success === true && 'data' in result) {
console.log('Success:', result.data);
} else if (result.success === false && 'error' in result) {
console.error('Error:', result.error);
}
}
}
Real-World Use Cases
API Response Handling
External APIs are the perfect use case for unknown. You control the request but not the response format, which might change or include unexpected data.
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
if (!isUser(data)) {
throw new Error('Invalid user data from API');
}
return data;
}
This pattern catches API contract violations immediately rather than allowing malformed data to propagate through your application.
Error Handling
JavaScript’s catch blocks can receive anything—errors, strings, numbers, or objects. TypeScript 4.0+ defaults to unknown for catch clause variables:
async function riskyOperation() {
try {
await someAsyncOperation();
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Error message:', error.message);
console.error('Stack trace:', error.stack);
} else if (typeof error === 'string') {
console.error('String error:', error);
} else {
console.error('Unknown error:', error);
}
}
}
JSON Parsing
JSON.parse() returns any by default, but you should treat it as unknown:
function parseConfig(jsonString: string): Config {
const parsed: unknown = JSON.parse(jsonString);
if (
typeof parsed === 'object' &&
parsed !== null &&
'apiUrl' in parsed &&
'timeout' in parsed &&
typeof (parsed as any).apiUrl === 'string' &&
typeof (parsed as any).timeout === 'number'
) {
return parsed as Config;
}
throw new Error('Invalid configuration format');
}
Migration Strategy: From any to unknown
Refactoring existing code from any to unknown should be gradual. Start with high-value areas like API boundaries and error handling.
Before:
function handleApiResponse(response: any) {
return {
userId: response.user.id,
userName: response.user.name
};
}
After:
interface ApiResponse {
user: {
id: number;
name: string;
};
}
function isApiResponse(value: unknown): value is ApiResponse {
return (
typeof value === 'object' &&
value !== null &&
'user' in value &&
typeof (value as any).user === 'object' &&
'id' in (value as any).user &&
'name' in (value as any).user
);
}
function handleApiResponse(response: unknown) {
if (!isApiResponse(response)) {
throw new Error('Invalid API response format');
}
return {
userId: response.user.id,
userName: response.user.name
};
}
Enable ESLint rules to prevent new any usage:
{
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-member-access": "error"
}
}
Best Practices and Common Pitfalls
Prefer unknown over any by default. If you don’t know a type, unknown forces you to handle that uncertainty safely. Reserve any for genuinely dynamic scenarios or when integrating with untyped JavaScript libraries during migration.
Avoid type assertions without validation. Writing data as User bypasses safety just like any. If you’re going to assert, validate first with a type guard.
Leverage TypeScript’s control flow analysis. Once you’ve narrowed a type with if checks, TypeScript remembers that within the block. Don’t redundantly check or cast.
Create reusable type guards. Instead of inline type checking, build a library of type guard functions. They’re testable, reusable, and self-documenting.
Remember that unknown is not assignable to other types. This is the point—you must narrow it first. If you find yourself fighting this, you’re probably trying to circumvent safety.
The choice between any and unknown reflects your commitment to type safety. any is expedient but dangerous. unknown requires more code upfront but catches bugs before they reach production. In any codebase that will live longer than a weekend, that’s a trade worth making.