Three special types that are frequently confused, and behave completely differently:
let a: any = getData();
a.whatever.deeply.nested(); // compiles — no safety at all, any error surfaces only at runtime
let u: unknown = getData();
u.whatever; // compile error — must narrow first
if (typeof u === "object" && u !== null && "whatever" in u) {
// now safely narrowed enough to access u.whatever
}
function fail(): never {
throw new Error("always throws — this function never returns normally");
}
| Type | Meaning | Can you call methods on it directly? |
|---|---|---|
any | Opts a value out of type checking entirely | Yes — no safety at all |
unknown | "Some value, type not yet known" | No — must narrow first |
never | A value that can never actually occur (an exhausted union, an always-throwing function's return) | N/A — no value of type never can exist at runtime |
any is contagious — once a value is any, everything derived from it becomes any too, silently disabling checking across an entire chain of calls. unknown is the type-safe alternative for "I don't know this type yet" (most commonly: data from JSON.parse, a third-party library with weak types, or a catch clause's error): it forces you to narrow before doing anything with the value, catching exactly the mistakes any would silently let through. never shows up less as something you write directly and more as something the compiler infers — most usefully, in the exhaustiveness-check pattern from the discriminated unions section above, where reaching never proves every case was actually handled.
Runtime validation still matters. Types are erased at compile time (see the first section) — casting JSON.parse(response) as User doesn't make the parsed value actually a User if the API returns something different; it just tells the compiler to stop checking. Anything crossing a real boundary (an HTTP response, form input, localStorage) needs actual runtime validation — a schema library (Zod, Yup, io-ts) is the standard real-world answer, often paired with deriving the static type directly from the runtime schema so the two can never drift apart.