Understanding the difference
If you've shipped production code, you've probably reached for any when TypeScript got in your way. It feels like a relief—the type errors disappear, and you move on. But that relief comes at a cost. The difference between unknown and any in TypeScript isn't just semantic; it's about whether your code stays safe or silently breaks six months later. This unknown vs any: a practical JavaScript & TypeScript guide walks you through when to use each, and more importantly, why choosing wrong compounds technical debt.
Both any and unknown represent values whose type isn't known at compile time. The critical difference: any disables type checking entirely, while unknown forces you to narrow the type before you use it. Understanding this distinction will make you a more effective TypeScript developer, especially when dealing with external APIs, user input, or legacy code.
Any spreads silently through code
The insidious part about any is that it propagates. Once a value is any, TypeScript stops caring about it—and anything that touches it. Watch how quickly this compounds:
function parseUser(data: any) {
const user = {
id: data.id,
email: data.email,
age: data.age,
};
return user;
}
const result = parseUser(someApiResponse);
// result is now { id: any; email: any; age: any; }
// Down the line, in a different file:
function sendEmail(user: { id: number; email: string }) {
console.log(`User ${user.id} at ${user.email}`);
// No error. TypeScript allows any to be assigned to any type.
// At runtime? user.email might be null.
}
sendEmail(result);
You see the problem immediately when you write it out like this, but in real codebases this happens across files, modules, and weeks. A junior dev adds a new function that accepts any to "fix a type issue." Someone else uses that function. The type information is now permanently corrupted. By the time you hit production, you've got a chain of unverified assumptions.
The mental model matters too. When you use any, you're telling TypeScript: "I know more than you do." Usually, you don't. You're just tired of fighting the compiler. But the compiler was fighting to protect you.
Unknown forces narrowing before use
unknown is any's responsible older sibling. It also means "we don't know the type," but it forces you to figure it out before doing anything dangerous:
function parseUser(data: unknown) {
// Can't access data.id directly. TypeScript won't allow it.
// data.email // Error: Object is of type 'unknown'.
// You have to narrow first:
if (typeof data === 'object' && data !== null) {
const obj = data as Record;
const id = obj.id;
const email = obj.email;
// id and email are still unknown. Keep narrowing.
if (typeof id === 'number' && typeof email === 'string') {
return { id, email };
}
}
throw new Error('Invalid user data');
}
This is more verbose. Yes. But notice what happens: type errors are impossible. You can't accidentally pass a narrowed unknown value into a function expecting a specific type without TypeScript knowing it's safe. The narrowing is explicit and verifiable.
In practice, you'll use guards and type predicates to make this cleaner:
function isUser(value: unknown): value is { id: number; email: string } {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as Record).id === 'number' &&
typeof (value as Record).email === 'string'
);
}
function parseUser(data: unknown) {
if (isUser(data)) {
return data; // data is now { id: number; email: string }
}
throw new Error('Invalid user data');
}
Now you've shifted the burden from "hope the caller used it right" to "verify the data matches what we expect." That's a massive difference in reliability.
Use unknown for external data
The strongest use case for unknown is anywhere data enters your system from outside: API responses, file reads, user input, database queries before validation, browser APIs.
Consider an e-commerce scenario where you're fetching product data from a third-party vendor API:
// Bad: any
async function fetchProduct(id: string): Promise {
const response = await fetch(`/api/products/${id}`);
return response.json();
}
// The response could be anything. Your calling code has no defense.
const product = await fetchProduct('123');
console.log(product.price * product.quantity); // Runtime error waiting to happen
// Better: unknown
async function fetchProduct(id: string): Promise {
const response = await fetch(`/api/products/${id}`);
return response.json();
}
// Caller must validate
const data = await fetchProduct('123');
if (
typeof data === 'object' &&
data !== null &&
typeof (data as Record).price === 'number' &&
typeof (data as Record).quantity === 'number'
) {
console.log(data.price * data.quantity);
} else {
console.error('Invalid product data');
}
The external API could change tomorrow. A field might suddenly be optional, or a number might come back as a string. With any, your code breaks silently until a customer notices. With unknown, you've already built the validation. You might need to update it, but you'll catch the break immediately during testing.
The same logic applies to environment variables, CLI arguments, request bodies, or anything else that comes from outside your codebase. Treat the boundary as contaminated until proven otherwise.
Ban any with a lint rule
If you're serious about type safety, add this to your eslintrc or TypeScript configuration:
// .eslintrc.json or similar
{
"rules": {
"@typescript-eslint/no-explicit-any": "error"
}
}
This turns any into a build error. When someone reaches for it, they'll be forced to either:
- Use
unknownproperly and narrow the type - Use a more specific type that actually describes what they're handling
- Add an explicit
eslint-disablecomment with a reason (which creates accountability)
In a team setting, you'll be amazed how often option 1 or 2 wins. Developers are lazy when given tools to be lazy, but they're also resourceful when forbidden from taking shortcuts. The extra 30 seconds to write a proper type guard often reveals assumptions that were wrong anyway.
If you have legacy code where any is everywhere, start with warn instead of error and gradually tighten it as you fix existing violations. Don't expect to fix everything overnight, but make it a priority during refactoring work.
Common mistakes
- Casting unknown to a specific type without checking:
const user = data as Useris justanywith extra steps. Ifdatais actually a string, you've hidden the error. Always use type guards or runtime validation. - Using unknown everywhere "just to be safe":
unknownshould be at system boundaries. Internal functions between modules you control can usually use more specific types. Overusing it creates type-narrowing boilerplate that obscures the actual logic. - Forgetting that unknown can still be passed around: An
unknownvalue won't give you type errors when you pass it to another function expectingunknown. You still need validation. The difference is TypeScript will error if you try to pass it to a function expecting something concrete. - Conflating unknown with null checking:
unknowndoesn't mean "might be null." It means "might be anything." You still need explicit null checks and type guards. They're complementary concerns.
Key takeaways
anydisables type checking and silently spreads corruption through your codebase.unknownforces you to verify the type before use.- Use
unknownat system boundaries—external APIs, user input, file I/O, anything you don't control. - Implement type guards and validation functions to narrow
unknownto specific types safely. - Enforce a lint rule against
anyin new code. Treat it as a code smell that requires justification. - The extra verbosity of
unknownup front saves debugging time later. It's an investment, not a tax.






Comments
Loading comments…