Choosing unknown over any is one of the biggest differences between safe and leaky TypeScript.
`any` opts a value out of type checking. You can call anything on it, assign it anywhere, and the compiler stays silent. Worse, any spreads: const n = anyValue.count is also any, and so on through your program. It exists for migrating JavaScript and for rare escape hatches; with noImplicitAny (part of strict) the compiler at least stops inventing it for unannotated parameters.
`unknown` is the safe "could be anything" type. Every value can be assigned to unknown, but you can't do anything with an unknown until you narrow it:
function handle(data: unknown) {
data.toUpperCase(); // error
if (typeof data === "string") data.toUpperCase(); // ok
}Use unknown for data whose shape you haven't verified: parsed JSON, postMessage payloads, values caught in catch (with strict, catch (e) gives e: unknown). Validate it — by hand or with a schema library — and the narrowed result is properly typed.
`never` is the empty type: no value has it. It shows up as:
- the return type of functions that always throw,
- what's left after narrowing has eliminated every possibility,
- the result of impossible intersections like
string & number.
That second use powers exhaustiveness checks: in the default branch of a switch over a union, assign the value to a never variable. If someone later adds a new member to the union, that assignment stops compiling and points you to the switch you forgot to update.
Rule of thumb: never write any when unknown would do.
Example
function parsePort(raw: string): number {
const data: unknown = JSON.parse(raw); // don't trust it yet
if (typeof data === "object" && data !== null && "port" in data &&
typeof data.port === "number") {
return data.port; // narrowed to number
}
throw new Error("invalid config");
}
type Status = "active" | "paused" | "closed";
function label(s: Status): string {
switch (s) {
case "active": return "Running";
case "paused": return "On hold";
case "closed": return "Done";
default: {
const unreachable: never = s; // breaks if Status gains a member
return unreachable;
}
}
}
try { parsePort("{}"); } catch (e) { // e: unknown
console.log(e instanceof Error ? e.message : String(e), label("paused"));
}Remember
anydisables checking and spreads; avoid itunknownaccepts anything but must be narrowed before usenever= no possible value; use it for exhaustiveness checks
Watch out for
- Typing parsed JSON as
anyand letting unchecked data flow through the program.