Lesson 10 of 25
unknown, any, and Type Guards
Why any silently turns off type checking and unknown doesn't, and how to write your own reusable type guard functions with the is keyword.
Every type covered so far describes something known. This lesson covers the
two types for describing something not known — any and unknown — and
why one of them should be treated as an emergency exit, not a default.
any: type checking, turned off
any is compatible with every type, in both directions — a value typed
any can be assigned anywhere, and anything can be assigned to it. This
sounds convenient. In practice, it means the compiler stops checking
anything that touches it:
let value: any = fetchFromSomewhere();
value.whatever.deeply.nested.chain(); // compiles — no error, ever
value + value.toUpperCase() * 2; // also compiles
const user: { name: string } = value; // compiles, no check at allNone of those lines are safe, and TypeScript won't warn you about a single
one, because any is contagious — it turns off checking not just for
itself, but for everywhere it flows into. A single un-annotated function
parameter (which defaults to any unless noImplicitAny is on) can quietly
erase type safety across an entire call chain.
unknown: type-safe "I don't know yet"
unknown also accepts any value, but — unlike any — nothing can be done
with an unknown value until you've proven what it actually is:
let value: unknown = fetchFromSomewhere();
value.toUpperCase();
// 'value' is of type 'unknown'.
if (typeof value === "string") {
value.toUpperCase(); // fine — narrowed to string
}This is the type-safe version of "I don't know what this is yet": it forces
the narrowing step that any lets you skip. unknown is the correct type
for anything genuinely untrusted — the result of JSON.parse(), the body of
an incoming request, a caught error, or third-party data you haven't
validated.
try {
riskyOperation();
} catch (error) {
// error is unknown under modern TypeScript defaults
if (error instanceof Error) {
console.log(error.message);
}
}Custom type guards
typeof, instanceof, and in cover a lot of narrowing, but sometimes the
check you need is more specific than any of them — validating an object's
shape, say. A function can be written as a type guard using a special
return type, value is Type:
type User = { name: string; age: number };
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"age" in value &&
typeof (value as User).name === "string" &&
typeof (value as User).age === "number"
);
}
function greet(value: unknown) {
if (isUser(value)) {
console.log(`Hello, ${value.name}`); // value narrowed to User here
}
}The value is User return type is what makes this a type guard rather
than a regular boolean function — it tells the compiler "when this returns
true, narrow the argument to User in the caller's branch," the exact
same mechanism as a built-in typeof check, just one you defined yourself.
Without that annotation, the function would just return boolean, and
calling it wouldn't narrow anything.
Where this matters most: the untrusted boundary
The realistic place all of this comes together is parsing external data —
an API response, localStorage, a form submission:
async function fetchUser(id: string): Promise<User | null> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json(); // json() is typed any by lib.dom.d.ts,
// but treat it as unknown deliberately
return isUser(data) ? data : null;
}Note the comment: response.json() is actually typed Promise<any> in
TypeScript's own DOM library definitions, for historical reasons — nothing
stops you from writing const data = await response.json() and getting
any back. Explicitly annotating it unknown and running it through a type
guard is a deliberate choice to keep the safety unknown provides, rather
than silently inheriting any from a library type you don't control.
Try it yourself
What to remember
anydisables type checking entirely, and that loss of safety spreads to everywhere the value flows — treat it as a last resort, not a convenience.unknownaccepts any value too, but requires narrowing before any operation is allowed on it — the safe version of "I don't know what this is yet."- A function can be a reusable, custom narrowing check by declaring its return type as
value is Type— a type guard. - Data crossing an untrusted boundary (API responses, parsed JSON, caught errors) should be typed
unknownand narrowed, even where a library type technically hands youany.
Check yourself
4 questions · pass 3/4 to unlock Generics Fundamentals
1.Given
let value: any = fetchSomething();, what doesvalue.whatever.deeply.nested()do at compile time?2.Given
let value: unknown = fetchSomething();, what happens if you immediately writevalue.toUpperCase()?3.What does the return type
value is stringon a function signal to the compiler?4.Which is the recommended type for a value whose shape you genuinely don't know yet, such as an untrusted API response?
4 left to answer