any vs unknown vs never
any stops the checking and spreads; unknown lets anything in and nothing out unchecked; never has no values, so a leftover becomes an error.
Predict first
Before you read: predict
ExamHall proctor console, compiled with tsc under strict: true. The live feed passes each WebSocket message's text to this handler:
Which lines does tsc report?
function onFeed(raw: string, showSeat: (seat: number) => void) { const m1: any = JSON.parse(raw); const m2: unknown = JSON.parse(raw); showSeat(m1.seat); // line 4 showSeat(m2.seat); // line 5 showSeat(m1.seta); // line 6}The 30-second version
any switches checking off, and everything read, called or returned from it is any too, unreported even under strict. unknown accepts every value but allows no use until narrowed; a strict catch variable is one. never has no values: it vanishes from unions, and assertNever turns a leftover member into a build error.
any stops the checking and spreads; unknown lets anything in and nothing out unchecked; never has no values, so a leftover becomes an error.
How it works
Step 1 of 10
any switches checking off. Every read, call and assignment on an any compiles, so a misspelt name or a wrong type goes unreported.In the kitchen
- a load stamped "cleared, no inspection" passes every station unexamined → an
anyvalue: nothing on it is checked (step 1)
Analogy: A recycling plant's sorting line
Loads are tipped onto an intake belt, and stations along the line pull out paper, glass and metal into bales.
| At the plant | In TypeScript |
|---|---|
| a load stamped "cleared, no inspection" passes every station unexamined | an any value: nothing on it is checked (step 1) |
| whatever is split off that load carries the same stamp | members, results and returns of an any are any (step 2) |
| the paper mill finds the plastic in the bale | the mistake shows at run time, not in the build |
| every load on the intake belt is accepted, but counts as unidentified until a station identifies it | unknown: any value goes in; narrow before use (step 5) |
| items thrown out by a jammed machine land in the reject tub, unidentified | a strict catch variable is unknown (step 6) |
| the end chute after the last station must stay empty | never: what is left once every member is handled (steps 7–8) |
| the design review flags the end chute when a new material joins the intake list with no station | assertNever fails the build on a new member (step 9) |
| a "mixed residue" bale takes whatever is left | a catch-all default hides the new member (step 10) |
In real terms: any is a value the compiler stops looking at, unknown is a value it will not let you use until you prove what it is, and never is the type with no values at all.
Breaks down when:
- A station can misidentify an item, and staff notice. TypeScript trusts the check you wrote and checks nothing at run time (
ts-runtime-boundary-validation). Wrong inference: "a narrowed value is guaranteed to be what its type says." - The stamp on a load is written by a person. TypeScript's spread needs nobody:
JSON.parsealready returnsany(step 3).
Contrasting case: bales that arrive already sorted by material from the plant's own pre-sorting hall, so every station knows what each one holds: a declared type, checked everywhere.
Examples, run for real
4.1 ExamHall: the proctor console live feed
EXH-1530: the backend renamed the feed message's seatNo to seat. The console's handler parsed each message into an any, the build stayed green, and every candidate tile read "Seat undefined". MR !1730 types the parsed message unknown and narrows it with a guard before any read.
const msg: any = JSON.parse(data); // before; after MR !1730: const msg: unknown
return `Seat ${msg.seatNo}`;
4.1 msg: any | tsc: no diagnostics | node prints: Seat undefined
4.1 msg: unknown | tsc: TS18046 at ex1-unknown.ts:3
4.1 narrowed first | tsc: TS2551 at ex1-narrowed.ts:8
4.1 reads msg.seat | tsc: no diagnostics | node prints: Seat 12
With any, the stale field name compiles and prints undefined (steps 1, 3). With unknown, the read itself is refused (step 5). Once a guard has narrowed msg to the declared message type, the stale name is reported (step 5); how the guard narrows is ts-narrowing-discriminated-unions.
4.2 Proof domain, catalog: a stock badge
badge(s: Stock) switches over "in_stock" | "low" | "sold_out" and ends with default: return assertNever(s). Then the catalog adds a "preorder" state.
4.2 three states | s in default: never | tsc: no diagnostics | node prints: Only a few left
4.2 "preorder" added | s in default: "preorder" | tsc: TS2345 at ex2-preorder.ts:12
With every state handled, s in the default is never, and the call compiles (step 8). The new state is left over, so the call is an error, naming "preorder", before anything ships (step 9).
Picture it

Try it
Answer first, then check. Nothing is saved.
Question 1 of 5
A basket page reads its total, in cents, through a helper. strict: true. The build stops at any tsc error; otherwise the built script is run with Node.
What happens?
function totalFrom(raw: string) { const row: any = JSON.parse(raw); return row.total;} function withShipping(cents: number): number { return cents + 500;} const total = totalFrom('{"basket":"b-12","total":"1999"}'); // line 10console.log(withShipping(total)); // line 11Question 2 of 5
A chat client refreshes its rooms and its unread count. strict: true.
Which lines does tsc report?
interface ChatApi { loadRooms(): Promise<void>; loadUnread(): Promise<number> } async function refresh(api: ChatApi, toast: (text: string) => void) { try { await api.loadRooms(); } catch (err) { toast(err.message); // line 7 } api.loadUnread().catch((err) => toast(err.message)); // line 9}Question 3 of 5
The proctor console's feed handlers compile clean. An MR adds line 4 and changes nothing else. strict: true.
What does tsc report after the MR?
type FeedMsg = | { kind: "candidate.flagged"; seat: number } | { kind: "candidate.submitted"; seat: number } | { kind: "candidate.paused"; seat: number }; // line 4, added by the MR function assertNever(x: never): never { throw new Error("unhandled " + JSON.stringify(x));} function tileColour(m: FeedMsg): string { if (m.kind === "candidate.flagged") return "red"; if (m.kind === "candidate.submitted") return "green"; return assertNever(m);} function alertSound(m: FeedMsg): string { switch (m.kind) { case "candidate.flagged": return "chime"; case "candidate.submitted": return "none"; default: return "none"; }}Question 4 of 5
A ticket app's helper is called wherever a seat arrives from the server. strict: true, and tsc reports nothing for this file, although line 13 misspells number.
Which single change makes tsc report line 13 and no other line?
interface Seat { row: string; number: number; price: number }declare function render(row: Seat["row"], n: Seat["number"]): void;declare function charge(amount: Seat["price"]): void; function parseSeat(raw: string) { const data: any = JSON.parse(raw); return data;} const s = parseSeat(body);render(s.row, s.number); // line 11charge(s.price); // line 12render(s.row, s.nubmer); // line 13Question 5 of 5
The proctor console keeps its tiles in tiles, a Map<string, Tile>. A helper fallbackTile supplies the right-hand side when a seat has no tile. strict: true.
Which declaration of fallbackTile lets this line compile?
const tile: Tile = tiles.get(seatId) ?? fallbackTile(`no tile for ${seatId}`);Go deeper
Pro tips
- Make new
anyvisible in review: search the diff for: anyandas anybefore you approve it. Ananyadded to "fix" an error hides every error after it (steps 1–2).
When: reviewing an MR that silences a type error. Verify (RAN,grep -nE ':\s*any\b|as any\b' cases/ex1-any.tstip-grep-any.sh, 50 runs, 1 distinct):2: const msg: any = JSON.parse(data);. Status: RAN (Git Bashgrep). - Turn on typescript-eslint's
no-unsafe-member-accessso a read from ananyis flagged where it happens. When: a codebase that already hasanyvalues flowing in. Verify: its documentation describes it as "Disallow member access on a value with typeany". Status: DOC (the plugin is not installed here).
Source
- TypeScript Handbook, "Everyday Types", section "any": https://www.typescriptlang.org/docs/handbook/2/everyday-types.html · fetched 2026-09-30. Quoted: "When a value is of type
any, you can access any properties of it (which will in turn be of typeany)" and "Usinganydisables all further type checking". Supports steps 1–2. Valid as of 2026-09-30. - TypeScript Handbook, "More on Functions", sections "unknown" and "never": https://www.typescriptlang.org/docs/handbook/2/functions.html · fetched 2026-09-30. Quoted: "it's not legal to do anything with an
unknownvalue", and "neveralso appears when TypeScript determines there's nothing left in a union." Supports steps 5 and 8. Valid as of 2026-09-30. - TypeScript Handbook, "Narrowing", sections "The never type" and "Exhaustiveness checking": https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type · fetched 2026-09-30. Quoted: "The
nevertype is assignable to every type; however, no type is assignable tonever(exceptneveritself)." Supports steps 7 and 9. Valid as of 2026-09-30. - TypeScript 3.0 release notes, "New unknown top type": https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type · fetched 2026-09-30. Quoted: "Anything is assignable to
unknown, butunknownisn't assignable to anything but itself andanywithout a type assertion or a control flow based narrowing." Supports step 5 and the alternate probe. Valid as of 2026-09-30. - TSConfig reference,
useUnknownInCatchVariablesandstrict: https://www.typescriptlang.org/tsconfig/#useUnknownInCatchVariables · fetched 2026-09-30. The flag changes "the type of the variable in a catch clause fromanytounknown", and it is in the list of flagsstrictenables.noImplicitAnyissues an error "whenever it would have inferredany", which is why ananyspread from an explicitanyis not reported (step 2). Valid as of 2026-09-30. - The compiler's own standard library,
typescript@6.0.3lib/lib.es5.d.tsandlib/lib.dom.d.ts(read 2026-09-30):parse(text: string, reviver?: …): any;,catch<TResult = never>(onrejected?: ((reason: any) => …, andjson(): Promise<any>;. Supports steps 3 and 6. - Pro tip 2: typescript-eslint, "no-unsafe-member-access": https://typescript-eslint.io/rules/no-unsafe-member-access/ · the site did not respond on 2026-09-30, so the rule text was read from its documentation source, https://raw.githubusercontent.com/typescript-eslint/typescript-eslint/main/packages/eslint-plugin/docs/rules/no-unsafe-member-access.mdx (fetched 2026-09-30): "Disallow member access on a value with type
any" and "This rule disallows member access on any variable that is typed asany." Supports pro tip 2 only.
Related lessons
Checked against the tools and docs as of 2026-09-30.