3.8 unknown: The Type-Safe Alternative to any
Alright, let’s talk about any’s smarter, more responsible cousin: unknown. If any is the developer who hacks everything together with duct tape and a prayer, unknown is the one who actually reads the instruction manual first. It’s a core tool for writing type-safe code, especially when you’re dealing with data from the outside world, like API responses, user input, or file contents—places where the type isn’t guaranteed.
Here’s the fundamental truth about unknown: you can assign anything to a variable of type unknown. A string, a number, a complex object, a Promise of a bag of chips—whatever. It’s the “top type” in the type system, meaning every other type can be assigned to it. This is its superpower and its initial frustration.
let myUnknownVariable: unknown;
// All of these are perfectly valid.
myUnknownVariable = "Hello World";
myUnknownVariable = 42;
myUnknownVariable = { some: "property" };
myUnknownVariable = [1, 2, 3];
So far, this sounds exactly like any, right? Here’s the crucial difference, and it’s why unknown is type-safe and any is a chaos demon: You cannot do anything with a value of type unknown. You can’t call it, you can’t read a property from it, you can’t use it in an arithmetic operation. The TypeScript compiler will immediately stop you. It forces you to prove you know what you’re dealing with before it lets you proceed.
let value: unknown = "Hello";
// These will all cause compiler errors:
// console.log(value.toUpperCase()); // Error: Object is of type 'unknown'.
// let newValue = value + 10; // Error: Object is of type 'unknown'.
// value(); // Error: Object is of type 'unknown'.
// You must first narrow the type.
if (typeof value === "string") {
console.log(value.toUpperCase()); // Now it works! Type is narrowed to 'string'.
}
Why You Should Care
Using any is like telling the compiler, “I know what I’m doing, shut up and get out of my way.” The compiler obliges, turns off all type-checking for that value, and you’re left to face the runtime consequences alone. unknown, on the other hand, is like saying, “I acknowledge this is potentially dangerous, so I promise to check it before I use it.” The compiler becomes your helpful partner, ensuring you keep that promise. This prevents a whole class of runtime errors like undefined is not a function or cannot read property 'x' of null.
The Standard Practice: Narrowing with Type Guards
The entire point of unknown is to force you to narrow its type. You do this with type guards. It’s a deliberate, safety-conscious process.
function processData(data: unknown) {
// Pitfall: Not checking first.
// const length = data.length; // Compiler Error. Good!
// Best Practice: Narrow the type.
if (Array.isArray(data)) {
// Now TypeScript knows 'data' is an array inside this block.
console.log(`The array has ${data.length} items.`);
} else if (typeof data === "string") {
console.log(data.toUpperCase());
} else if (
typeof data === "object" &&
data !== null &&
"message" in data
) {
// A more complex guard for objects
console.log(`Message: ${data.message}`);
} else {
console.log("I have no idea what this is.", data);
}
}
When to Reach for unknown Instead of any
You should default to unknown in these key scenarios:
Function parameters for generic data parsers: Think of a function that parses JSON. The return type shouldn’t be
any; it should beunknownbecause you literally don’t know what’s in that string until you check it.function safeParse(jsonString: string): unknown { try { return JSON.parse(jsonString); } catch { return null; // or handle the error } } const parsedData = safeParse('{"name": "Alice", "age": 30}'); // parsedData is 'unknown', so we MUST check its shape. if ( typeof parsedData === "object" && parsedData !== null && "name" in parsedData && typeof parsedData.name === "string" ) { console.log(`Hello, ${parsedData.name}`); }Catching errors in try/catch blocks: In TypeScript, the
catchclause variable is…any. This is one of the language’s few genuinely questionable choices. We can fix it.try { somethingThatMightThrow(); } catch (err) { // 'err' is of type 'any' :( // Let's immediately cast it to 'unknown' const error = err as unknown; // Now we can safely check what it is. if (error instanceof Error) { console.log(error.message); } else { console.log("An unknown error occurred", error); } }
The One Annoying Edge Case
Sometimes you know what the type is (e.g., you’re receiving data from a trusted source), and the compiler’s pedantry is just slowing you down. In these cases, you can use a type assertion to tell the compiler the exact type. Use this sparingly and deliberately, as it’s a conscious decision to bypass the safety unknown provides.
const dataFromTrustedSource: unknown = getTheData();
// I'm 100% sure this is an array of numbers.
const myData = dataFromTrustedSource as number[];
// Now I can use it, but I'm on the hook if I'm wrong.
console.log(myData[0].toFixed(2));
The beauty of unknown is that it makes this bypass an explicit, obvious choice (as number[]), rather than the silent, default danger of any. It moves the potential point of failure from a subtle, hard-to-find bug to a single, auditable line of code. And that, my friend, is how you write robust software without losing your mind.