AniUI Academy

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.

10 min read

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 all

None 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

Try it yourself
Loading playground...

What to remember

  • any disables type checking entirely, and that loss of safety spreads to everywhere the value flows — treat it as a last resort, not a convenience.
  • unknown accepts 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 unknown and narrowed, even where a library type technically hands you any.

Check yourself

4 questions · pass 3/4 to unlock Generics Fundamentals

up to 50
  1. 1.Given let value: any = fetchSomething();, what does value.whatever.deeply.nested() do at compile time?

  2. 2.Given let value: unknown = fetchSomething();, what happens if you immediately write value.toUpperCase()?

  3. 3.What does the return type value is string on a function signal to the compiler?

  4. 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