Skip to content

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 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
}

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 string
value.nonExistent(); // no error
value = 42; // no error

any is TypeScript’s escape hatch. It is appropriate in narrow situations:

  • Migrating existing JavaScript — annotate problematic values as any temporarily 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 fetchJsonresponse.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);
}

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.

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.

any ←── breaks out of the type system entirely
unknown ←── safe entry point for untyped data — must narrow before use
T ←── specific types: string, number, HighScore, TriviaQuestion, ...
never ←── the bottom — no value can ever have this type

In a scratch file exhaustive.ts (import QuizState from types.ts):

  1. Write an assertNever function: function assertNever(value: never): never — the body should throw a new Error with a message like 'Unhandled case' and the JSON-stringified value.
  2. Write a function describeState(state: QuizState): string using a switch on state.status that returns a string for each branch, calling assertNever(state) in the default case.
  3. Temporarily add | { status: 'paused' } to QuizState in types.ts. Observe the compiler error on assertNever(state). Then remove it.
  4. In Module 07, assertNever moves into main.ts and is used inside the state-handling switch — every unhandled QuizState member becomes a compile error automatically.
  • unknown is the safe type for values whose shape is not yet determined — narrow before use.
  • any disables type checking — use it only at migration boundaries or in explicit trust assertions.
  • never is the bottom type — it represents values that cannot exist.
  • assertNever(value: never): never creates 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.