unknown, any and never
The top type you must prove, the escape hatch that spreads, and the empty type that makes exhaustiveness checks work.
After this lesson you can
- Take untrusted data as unknown and narrow it before use
- Handle a caught error whose type is unknown
- Use never to make a forgotten case fail, at compile time and at runtime
Three special types, and the one-line definition of each:
unknown— any value at all, and you may do nothing with it until you narrow it. The safe type for "I do not know yet".any— any value at all, and you may do anything with it. Not a type so much as the checker switched off.never— no value at all. The type of a function that never returns (it throws, or loops forever), and what is left of a union once every member has been ruled out.
unknown at the boundary
Everything that enters a program from outside — a request body, a file, a
message from a queue — is unknown until checked. The trap is that several
standard APIs hand it to you as any: JSON.parse, response.json() in
the browser's typings, and anything typed loosely. An any can be assigned to any type without a
check, so the "validation" is just an annotation:
interface Order { id: string; quantity: number;} function isOrder(x: unknown): x is Order { return ( typeof x === "object" && x !== null && typeof (x as Record<string, unknown>).id === "string" && typeof (x as Record<string, unknown>).quantity === "number" );} export function run(): unknown[] { const body = '{"id":"A-1","quantity":"3"}'; // any: accepted as an Order without a single check. const trusted: Order = JSON.parse(body); const doubled = trusted.quantity * 2; // "3" * 2 happens to work... const plusOne = trusted.quantity + 1; // ...and "3" + 1 does not // unknown: nothing is usable until the guard has proved it. const raw: unknown = JSON.parse(body); const verdict = isOrder(raw) ? "valid order" : "rejected at the boundary"; return [doubled, plusOne, verdict];}Declare the parsed value as unknown instead, and the compiler refuses to
let you touch it until a guard (or a schema library such as Zod) has proved
its shape.
Caught errors are unknown
JavaScript lets you throw anything: an Error, a string, an object, a
number. Under strict (the useUnknownInCatchVariables flag) the variable
in catch (err) is therefore unknown, not Error. Casting it back is the
usual mistake:
function fail(kind: number): never { if (kind === 0) throw new Error("disk full"); if (kind === 1) throw "timeout"; throw { code: 503 };} export function run(): string[] { const out: string[] = []; for (const kind of [0, 1, 2]) { try { fail(kind); } catch (err) { // err is unknown under strict: anything at all can be thrown. const careless = (err as Error).message; const careful = err instanceof Error ? err.message : JSON.stringify(err); out.push(`${careless} | ${careful}`); } } return out;}Narrow with instanceof Error and decide what to do with everything else.
never, the exhaustiveness check
The never trick from the unions lesson has a runtime half. Data from outside
can contain a member your union never listed — the server shipped a new
status before your client did. An assertNever helper fails at compile
time when you forget a case you know about, and throws a clear error at
runtime for a case you did not:
type Payment = | { status: "paid"; amount: number } | { status: "failed"; reason: string }; function assertNever(value: never): never { throw new Error(`Unexpected value: ${JSON.stringify(value)}`);} function summary(p: Payment): string { switch (p.status) { case "paid": return `paid ${p.amount}`; case "failed": return `failed: ${p.reason}`; default: return assertNever(p); }} export function run(): string[] { // The server started sending a status this code was never told about. const fromServer: Payment[] = JSON.parse( '[{"status":"paid","amount":20},{"status":"refunded","amount":20}]', ); return fromServer.map((p) => { try { return summary(p); } catch (e) { return String(e); } });}Common mistakes
- Trusting
JSON.parse. Itsanyflows into whatever type you write. In the example,"3" * 2happened to work and"3" + 1gave"31": the same bad value is right in one place and wrong in the next. (err as Error).messagein a catch. For a thrown string or object that isundefined, and the log line says nothing.any"just for this one value". Everything derived from it isanytoo, so the hole grows silently through the code that uses it.- Swallowing the impossible case. A
default: return ""hides the new status; throwing viaassertNevermakes it loud.
What interviewers ask
unknownoranyfor an API response?unknown, then a runtime check (a guard or a schema) that produces a typed value.anyskips both the check and the type.- What is the type of the variable in a clause? with (part of ), because any value can be thrown; narrow with .
Exercise · Pro
A number from somewhere inside untrusted JSON
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade