Skip to content

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?

Code for this question
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}
Pick one

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 any value: 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.parse already returns any (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

A woman at a counter prepares to cut open a taped box, beside an open box of labelled packets, below a closed door in a green alcove.
An illustration to help it stick. The mechanism is in the sections above.

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?

Code for this question
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 11
Pick one

Question 2 of 5

A chat client refreshes its rooms and its unread count. strict: true.

Which lines does tsc report?

Code for this question
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}
Pick one

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?

Code for this question
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";  }}
Pick one

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?

Code for this question
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 13
Pick one

Question 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?

Code for this question
const tile: Tile = tiles.get(seatId) ?? fallbackTile(`no tile for ${seatId}`);
Pick one

Go deeper

Pro tips
  1. Make new any visible in review: search the diff for : any and as any before you approve it. An any added to "fix" an error hides every error after it (steps 1–2).
    grep -nE ':\s*any\b|as any\b' cases/ex1-any.ts
    When: reviewing an MR that silences a type error. Verify (RAN, tip-grep-any.sh, 50 runs, 1 distinct): 2: const msg: any = JSON.parse(data);. Status: RAN (Git Bash grep).
  2. Turn on typescript-eslint's no-unsafe-member-access so a read from an any is flagged where it happens. When: a codebase that already has any values flowing in. Verify: its documentation describes it as "Disallow member access on a value with type any". Status: DOC (the plugin is not installed here).
Source

Checked against the tools and docs as of 2026-09-30.