TypeScript's main job is to catch type bugs at compile time, before your code ever runs. But there are two types designed for the "I don't know the type yet" situation — any and unknown — and they behave very differently. One opts you out of the type system entirely. The other keeps you safe.
The problem with any
When you annotate something as any, you're telling TypeScript: "Trust me, I know what I'm doing — stop checking." TypeScript steps aside and lets you do whatever you want with that value, with zero verification.
function parseInput(input: any) {
return input.toUpperCase();
}
parseInput(21); // No compile error — but crashes at runtime
// TypeError: input.toUpperCase is not a function
TypeScript doesn't check that 21 is a number, not a string. It happily compiles the code. The crash happens at runtime — silently defeating the entire purpose of using TypeScript in the first place.
anydoesn't mean "flexible." It means "no type safety here." Everyanyis a hole in the type system through which bugs can silently enter.
The safer alternative: unknown
unknown is TypeScript's way of saying: "This value could be anything — and you must prove what it is before you use it." You cannot call methods or access properties on an unknown value without first narrowing its type.
function parseInput(input: unknown) {
return input.toUpperCase();
// Error: Object is of type 'unknown'
// TypeScript catches this at compile time
}
With unknown, the bug is caught before it ever reaches production. TypeScript forces you to narrow the type before doing anything with the value.
Type narrowing
Type narrowing is the mechanism TypeScript uses to refine an unknown or union type down to something specific. You perform a runtime check — a type guard — and TypeScript adjusts the inferred type within each branch of that check. The most common tools are typeof, instanceof, and custom type guard functions.
Using typeof
function formatValue(value: unknown): string {
if (typeof value === "string") {
return value.toUpperCase(); // TypeScript knows it's a string here
}
if (typeof value === "number") {
return value.toFixed(2); // TypeScript knows it's a number here
}
return "Unsupported type";
}
Each if block narrows the type. Outside those blocks, value is still unknown. Inside them, TypeScript knows exactly what it is and unlocks the appropriate methods.
Using instanceof
instanceof is useful when working with class instances — most commonly when handling errors, which are typed as unknown in modern TypeScript.
function handleError(error: unknown): string {
if (error instanceof Error) {
return error.message; // Safe — TypeScript knows it's an Error object
}
return "Something went wrong";
}
Without the instanceof check, accessing error.message would be a compile error. The check is the proof TypeScript needs.
Custom type guards
For complex objects, you can write a custom type guard function using the value is Type return syntax:
interface ApiResponse {
status: number;
data: string;
}
function isApiResponse(value: unknown): value is ApiResponse {
return (
typeof value === "object" &&
value !== null &&
"status" in value &&
"data" in value
);
}
function handleResponse(value: unknown) {
if (isApiResponse(value)) {
console.log(value.status); // TypeScript knows the full shape here
}
}
Whenever you reach for any, ask yourself: is this actually "I don't know the type" or is it "I don't want to deal with the type right now"? The first case calls for unknown with narrowing. The second is technical debt. unknown keeps the compiler working for you — any sends it home early.
Top comments (0)