TypeScript offers two main types for handling values whose type is not known in advance: any
and unknown
. While they may seem similar, these types have fundamental differences that make unknown
a safer choice in most situations.
The fundamental difference
The any
type completely disables the typing system for the concerned value, while unknown
is a type that requires explicit checks before any use.
// With any
let dataAny: any = JSON.parse('{"x": 10}');
console.log(dataAny.x); // OK
console.log(dataAny.nonExistent.property); // OK at compilation, error at runtime
// With unknown
let dataUnknown: unknown = JSON.parse('{"x": 10}');
console.log(dataUnknown.x); // Compilation error
The advantages of unknown
1. Increased type safety
With unknown
, you are forced to check the type before using a value, which greatly reduces runtime errors.
function processValue(value: unknown): string {
// Required to check the type before using value
if (typeof value === "string") {
return value.toUpperCase();
}
if (typeof value === "number") {
return value.toString();
}
return "Unsupported type";
}
2. More explicit code
Using unknown
makes code more explicit by forcing developers to document expected types through checks.
// With any, the real type remains implicit
function handleDataAny(data: any) {
return data.count * 2; // What happens if data.count is not a number?
}
// With unknown, the expected type is explicitly checked
function handleDataUnknown(data: unknown) {
if (typeof data === "object" && data !== null && "count" in data && typeof data.count === "number") {
return data.count * 2; // Safe
}
throw new Error("Invalid data format");
}
3. Better error detection
unknown
allows errors to be detected at compilation rather than runtime.
let obj: any = { name: "TypeScript" };
console.log(obj.getName()); // Error at runtime
let objUnknown: unknown = { name: "TypeScript" };
// console.log(objUnknown.getName()); // Compilation error
Practical examples of using unknown
1. Safe JSON parsing
function parseConfig(jsonString: string): { port: number } | null {
try {
const config: unknown = JSON.parse(jsonString);
if (
config &&
typeof config === "object" &&
"port" in config &&
typeof config.port === "number"
) {
return { port: config.port };
}
return null;
} catch {
return null;
}
}
2. API response handling
async function fetchUserData() {
const response = await fetch('https://api.example.com/user');
const data: unknown = await response.json();
if (
data &&
typeof data === "object" &&
"name" in data &&
typeof data.name === "string"
) {
return `Hello, ${data.name}!`;
}
return "Invalid user data";
}
3. Custom type guards
interface User {
id: number;
name: string;
}
// Custom type guard
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
typeof value.id === "number" &&
typeof value.name === "string"
);
}
function processUser(input: unknown) {
if (isUser(input)) {
// TypeScript now knows that input is of type User
console.log(`User: ${input.name}`);
}
}
When to use any
Here are situations where any
might be justified:
Integration with libraries without type definitions - When the typing effort would be excessive for an external library without types.
Gradual migration from JavaScript to TypeScript - To gradually convert an existing codebase without blocking everything.
Extreme cases where the type is truly unpredictable - Very rare, but can happen in specific contexts where even with checks, the type cannot be determined with certainty.
Conclusion
The unknown
type represents a safer approach than any
for handling values of unknown type in TypeScript. It offers a good balance between flexibility and type safety, allowing you to work with data of unknown structure while maintaining typing rigor.
By choosing unknown
by default and reserving any
for exceptional cases, you will get code that is more solid, easier to maintain, and less prone to runtime errors.
Top comments (0)