unknown, any, and never
TypeScript’s type system has three types that sit at its boundaries. unknown is the safe way to represent a value whose type is not yet determined. any turns off type checking entirely. never represents values that can never exist. Understanding all three — especially how and when to use each — is what separates good TypeScript from TypeScript that just satisfies the compiler.
unknown
Section titled “unknown”unknown is the type-safe counterpart to any. It says: “this value exists, but I do not know its type yet.” TypeScript will not let you do anything with an unknown value until you narrow it:
function processInput(value: unknown): string { value.toUpperCase(); // Error: 'value' is of type 'unknown' value.length; // Error return String(value); // ✓ — String() accepts any value}To use an unknown value, narrow it first:
function processInput(value: unknown): string { if (typeof value === 'string') { return value.toUpperCase(); // ✓ — narrowed to string } return String(value); // fallback for all other types}Where unknown appears in AceIt
Section titled “Where unknown appears in AceIt”Caught errors. In TypeScript with useUnknownInCatchVariables (part of strict), caught values are unknown:
try { const data = await fetchJson<ApiResponse>(url);} catch (err) { // err: unknown — anything can be thrown const message = err instanceof Error ? err.message : String(err); alert(message);}JSON.parse. The return type of JSON.parse is any in the standard library — but assigning it to unknown and then narrowing is the correct pattern:
const parsed: unknown = JSON.parse(raw);if (isHighScore(parsed)) { return parsed; // HighScore — safe}return null;Use unknown whenever a value arrives from outside the TypeScript boundary and its shape is not guaranteed.
any disables TypeScript’s type checking for a value. Every operation on an any is accepted without error:
let value: any = 'AceIt';value.toFixed(2); // no error — but crashes at runtime if value is a stringvalue.nonExistent(); // no errorvalue = 42; // no errorany is TypeScript’s escape hatch. It is appropriate in narrow situations:
- Migrating existing JavaScript — annotate problematic values as
anytemporarily while you add proper types. - Working with truly untyped third-party code — when a library has no type definitions and you cannot add them.
- The one cast inside
fetchJson—response.json() as Promise<T>uses the type system’s trust mechanism intentionally at a known boundary.
Avoid any in application code. Every any in AceIt is a missed opportunity to catch a bug at compile time. If you are tempted to use any, ask whether unknown with a type guard is possible first.
never is the bottom type — it represents values that can never exist. A function that always throws has a never return type. A variable that has been narrowed to a type that has no values is never.
function fail(message: string): never { throw new Error(message);}never for exhaustive checks
Section titled “never for exhaustive checks”The most useful application of never in AceIt is an exhaustive switch on a discriminated union. After handling every member of QuizState, the default case receives a never value — TypeScript can confirm that no cases were missed:
function assertNever(value: never): never { throw new Error(`Unhandled case: ${JSON.stringify(value)}`);}
function render(state: QuizState): void { switch (state.status) { case 'idle': showScreen('start-screen'); break; case 'loading': showScreen('loading-screen'); break; case 'active': renderQuestion(state.question, state.index, state.total); break; case 'done': renderResult(state.score, state.total, false, null); break; default: assertNever(state); // Error if a QuizState member is not handled above }}If you add a new QuizState member — say { status: 'paused' } — without adding a case 'paused' branch, TypeScript reports an error on assertNever(state): '{ status: "paused" }' is not assignable to never. The compiler tells you exactly where to add the missing case.
never in union narrowing
Section titled “never in union narrowing”After narrowing all branches of a union, the remaining type is never:
function display(value: string | number): void { if (typeof value === 'string') { console.log(value.toUpperCase()); } else if (typeof value === 'number') { console.log(value.toFixed(2)); } else { // value: never — all cases handled }}TypeScript uses never to confirm that every possible value has been accounted for.
The hierarchy
Section titled “The hierarchy”any ←── breaks out of the type system entirelyunknown ←── safe entry point for untyped data — must narrow before useT ←── specific types: string, number, HighScore, TriviaQuestion, ...never ←── the bottom — no value can ever have this typeExercise
Section titled “Exercise”In a scratch file exhaustive.ts (import QuizState from types.ts):
- Write an
assertNeverfunction:function assertNever(value: never): never— the body should throw anew Errorwith a message like'Unhandled case'and the JSON-stringified value. - Write a
function describeState(state: QuizState): stringusing aswitchonstate.statusthat returns a string for each branch, callingassertNever(state)in thedefaultcase. - Temporarily add
| { status: 'paused' }toQuizStateintypes.ts. Observe the compiler error onassertNever(state). Then remove it. - In Module 07,
assertNevermoves intomain.tsand is used inside the state-handling switch — every unhandledQuizStatemember becomes a compile error automatically.
unknownis the safe type for values whose shape is not yet determined — narrow before use.anydisables type checking — use it only at migration boundaries or in explicit trust assertions.neveris the bottom type — it represents values that cannot exist.assertNever(value: never): nevercreates exhaustive switch checking — adding a union member without handling it becomes a compile error.- After all union members are narrowed, the remaining type is
never— TypeScript uses this to verify completeness.