unknown and any look similar — both accept any value — but unknown forces you to narrow before doing anything with it, while any disables type-checking entirely for that value, confirmed by compiling both against a direct method call. That single difference is why unknown is the safe choice at untrusted boundaries and any effectively opts out of TypeScript.
typesunknownanynevertuplesliteral-types
Knowledge Check
12 questions · pass at 70%
0/12
easymcq
1. What's the key difference between `any` and `unknown`?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
LITERAL TYPES: a specific value AS a type
let dir: "up" | "down"; // only these two exact strings allowed
UNION (|) → could be EITHER type
INTERSECTION (&) → must satisfy BOTH types (combined properties)
any → DISABLES type-checking entirely for that value (and
everything derived from it) — use rarely, deliberately
unknown → accepts ANY value, but BLOCKS all operations until
narrowed via a real runtime check (typeof, etc.)
→ the SAFE alternative to any for uncertain values
never → a value that can NEVER occur (always-throws function,
or an already-exhaustively-handled case)
→ powers the never-based EXHAUSTIVENESS pattern:
const _exhaustive: never = value; // fails to compile
if a switch/union case is missed
number[] → any length, same-typed elements
[number, number] → TUPLE — FIXED length, per-position types
ENUM vs UNION OF LITERALS: prefer union-of-literals for new code
— fully ERASED (zero runtime footprint), JSON-friendly, avoids
enum's interop quirks. enum generates a REAL runtime object.
The beginner framing: beyond the familiar string/number/boolean, TypeScript has several more precise type-building blocks — literal types, unions, intersections, and a few special types (unknown, any, never) whose exact behavior matters a lot more than their similar-sounding names suggest.
let direction: "up" | "down" | "left" | "right";direction = "up"; // ✅ finedirection = "sideways"; // ❌ error — not one of the allowed literals
A literal type is a type containing exactly one specific value — "up" isn't just a string, it can be the type"up" itself. Combined with the union operator (|), literal types are how TypeScript expresses "one of these exact values," a common and precise alternative to enums (more on that below).
type Id = string | number; // UNION — could be EITHER typetype Loud = { volume: number };type Fast = { speed: number };type LoudAndFast = Loud & Fast; // INTERSECTION — has BOTH sets of propertiesconst car: LoudAndFast = { volume:
A union (|) means "this could be any one of these types." An intersection (&) means "this must simultaneously satisfy all of these types" — for object types, that means the resulting type has the combined properties of both.
unknown vs. any vs. never: three easily confused types#
let a: any = "hello";a.toUpperCase(); // ✅ compiles — `any` disables type-checking entirely for this valuea(); // ✅ ALSO compiles — even calling a string as a function, no error!let u: unknown = "hello";u.toUpperCase(); // ❌ Error: 'u' is of type 'unknown' — must narrow FIRSTif (typeof u === "string") { u.toUpperCase();
Confirmed by compiling both directly: any genuinely disables type-checking for that value — literally anything is allowed, including operations that will obviously fail at runtime (calling a string as if it were a function). unknown is the type-safe alternative for "I don't know this value's type yet" — it accepts any value being assigned to it, but refuses to let you do anything with it until you've narrowed it to something more specific via a runtime check.
function fail(message: string): never { throw new Error(message); // this function NEVER returns normally}type Status = "active" | "inactive";function handle(status: Status) { if (status === "active") return 1;
never represents a value that can genuinely never occur — a function that always throws, or (more usefully) the type TypeScript assigns to a variable once every possible case has already been handled. This confirmed pattern (assertUnreachable) is a real, practical exhaustiveness-checking technique: if someone adds a new value to the Status union later and forgets to handle it in handle, the call to assertUnreachable stops compiling — a real compile error catching a real missed case.
const scores: number[] = [10, 20, 30]; // ARRAY — any length, all elements same typeconst point: [number, number] = [1, 2]; // TUPLE — FIXED length, position-specific typesconst bad: [number, number] = [1
Confirmed: assigning a 3-element array to a [number, number] tuple fails to compile — tuples enforce both a fixed length and per-position types, unlike a regular array which only constrains the element type, not the count.
Enums vs. union-of-literals: a deliberate recommendation#
// enum — generates actual runtime code (an object)enum Color { Red, Green, Blue }// union of literals — ERASED entirely at compile time, zero runtime footprinttype ColorLiteral = "red" | "green" | "blue";
Both express "one of a fixed set of options," but modern TypeScript guidance generally favors the union-of-literals form for a few concrete reasons: it's purely erasable (no runtime object generated — consistent with the type-erasure model from TypeScript Basics), it works naturally with plain string values from APIs/JSON without a conversion step, and it avoids enum's well-documented interop quirks (numeric enums allowing any number to be assigned, reverse mappings). Enums aren't wrong, but a union of literals is the more common recommendation for new code — worth knowing as a deliberate choice, not just unfamiliarity with enums.
function processInput(value: unknown) { return value.toString(); // does this compile?}
Solution
No — this fails to compile with an error like 'value' is of type 'unknown'. Even though every value in JavaScript technically has a .toString() method, unknown requires an explicit narrowing check before TypeScript will let you call ANY method on it — it doesn't matter that .toString() happens to exist on virtually everything; unknown categorically blocks property/method access until narrowed. The fix would be something like if (typeof value === "object" || typeof value === "string") { return value.toString(); } or a similar narrowing check first.
Build a minimal exhaustiveness-checked handler using the never-based pattern:
type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number };function area(shape: Shape): number { switch (shape.kind) { case "circle":
Try adding a third variant like { kind: "triangle"; base: number; height: number } to the Shape union without adding a corresponding case — the const _exhaustive: never = shape line then fails to compile, since shape in the default branch would now include the un-handled triangle case, which isn't assignable to never.
The never-based exhaustiveness pattern here is a direct, practical extension of narrowing, covered in full depth in Type Narrowing & Guards — this topic's switch example is discriminated-union narrowing in action, one instance of a broader mechanism. And the enum-vs-union-of-literals recommendation follows directly from the type-erasure model established in TypeScript Basics — a union of literals stays true to "types have zero runtime footprint," while enum is a deliberate, narrow exception that generates real runtime code.
1. Using any as a shortcut to silence a type error, without realizing what it disables#
function process(data: any) { // ❌ silences the immediate error, but disables ALL checking on `data` return data.whatever.deeply.nested.access; // no error, even though this will crash at runtime}
any doesn't just relax one specific check — it disables type-checking entirely for that value, including for everything derived from it. unknown (with proper narrowing) almost always achieves the intended safety without this blanket opt-out.
2. Assuming a regular array and a tuple are interchangeable#
function midpoint(p: number[]) { // ❌ accepts arrays of ANY length, even empty ones return (p[0] + p[1]) / 2; // p[1] could be `undefined` if the array has fewer than 2 elements}
If a function genuinely expects exactly two numbers, [number, number] (a tuple) documents and enforces that at the type level — a plain number[] parameter accepts arrays of any length, silently allowing a caller to pass too few or too many elements.
3. Reaching for enum reflexively without considering the union-of-literals alternative#
enum Direction { Up, Down } // generates a real runtime object — is that actually needed here?
For most "one of a fixed set of values" cases, a union of string literals is erasable, JSON-friendly, and avoids enum's interop quirks — worth actively choosing rather than defaulting to enum out of habit.
Prefer unknown over any for genuinely uncertain values (parsed JSON, external API responses) — it forces safe narrowing rather than disabling checks entirely.
Use the never-based exhaustiveness pattern in switch statements over discriminated unions, so adding a new case later produces a compile error if it's not handled anywhere it should be.
Reach for tuples when a fixed-length, position-specific structure is the actual intent (coordinates, key-value pairs) rather than a same-typed array.
Default to union-of-literals over enum for new code, reserving enum for cases where its specific runtime-object behavior is genuinely wanted.
Union-of-literal types have zero runtime footprint (fully erased), while enum generates an actual runtime object — for code where bundle size or avoiding any extra runtime object matters even slightly, this is a concrete, measurable reason to prefer literals, not just a stylistic one.
unknown's narrowing requirement has no runtime performance cost of its own — the narrowing checks you write (typeof, etc.) are the same checks you'd need to write for safety regardless of whether the type system required them.
11
, speed:
200
};
// must satisfy BOTH
// ✅ fine now — narrowed to string
}
if (status === "inactive") return 2;
return assertUnreachable(status); // status is `never` HERE — both cases already handled
}
function assertUnreachable(x: never): never {
throw new Error("unreachable");
}
,
2
,
3
];
// ❌ error — 3 elements, tuple expects exactly 2
return
Math.
PI
*
shape.radius
**
2
;
case "square":
return shape.side ** 2;
default:
// if a THIRD shape kind is ever added and not handled above,
// `shape` here would NOT be `never`, and this line fails to compile: