Skip to content
Laviyx Learn

Try

TypeScript course menu

TypeScript any, unknown and never: The Special Types Explained

Understand TypeScript's special types: any turns off type checking, unknown is the safe type for values you have not checked yet, never represents values that cannot exist, plus how to handle JSON.parse, catch clauses and exhaustive checks.

Beginner8 min readUpdated
On this page

Three special types describe situations that normal types cannot: any (anything, unchecked), unknown (anything, but checked before use) and never (nothing can exist here).

any: switching off the checks#

A value of type any can be used in any way, and TypeScript checks nothing:

TypeScript
let data: any = "hello";
data = 42;
data = { name: "Asha" };

console.log(data.name);

try {
  console.log(data.address.city);   // TypeScript allows this...
} catch (error) {
  console.log("...but at runtime:", (error as Error).message);
}
Output
Asha
...but at runtime: Cannot read properties of undefined (reading 'city')

TypeScript accepted data.address.city without complaint, and it crashed when the program ran. any spreads, too: anything you get from an any is also any. Every any is a hole in your type safety.

any appears in two ways:

  • Explicitly, when you write it.
  • Implicitly, when TypeScript cannot work out a type, such as an unannotated parameter. Strict mode's noImplicitAny turns these into errors, which is why you saw Parameter 'n' implicitly has an 'any' type in Type Inference.

Use any only as a temporary escape hatch, for example while converting an old JavaScript file.

unknown: the safe alternative#

unknown also accepts any value, but you cannot use it until you check what it is:

TypeScript
const value: unknown = "hello";
console.log(value.toUpperCase());
Compiler error
error TS18046: 'value' is of type 'unknown'.

After a check, TypeScript knows the type inside that branch (this is called narrowing):

TypeScript
function describe(value: unknown): string {
  if (typeof value === "string") {
    return `a string of length ${value.length}`;
  }
  if (typeof value === "number") {
    return `the number ${value.toFixed(2)}`;
  }
  if (Array.isArray(value)) {
    return `an array with ${value.length} items`;
  }
  return "something else";
}

console.log(describe("hello"));
console.log(describe(3.14159));
console.log(describe([1, 2, 3]));
console.log(describe(null));
Output
a string of length 5
the number 3.14
an array with 3 items
something else

Use unknown for values from outside your program: parsed JSON, API responses, user input, and caught errors. It forces you to check before trusting them. Narrowing and Type Guards cover the checking tools.

JSON.parse#

JSON.parse returns any, which silently lets bad data through. Assigning it to unknown and checking is safer:

TypeScript
const raw = '{"name":"Asha","age":21}';
const parsed: unknown = JSON.parse(raw);

if (typeof parsed === "object" && parsed !== null && "name" in parsed && typeof parsed.name === "string") {
  console.log(parsed.name.toUpperCase());
}
Output
ASHA

Checks like this get long for big objects; in real projects, validation libraries such as Zod check data and produce the TypeScript type from one schema.

Errors in catch#

In strict mode, the variable in a catch clause is unknown, because anything can be thrown, not only Error objects:

TypeScript
try {
  JSON.parse("{oops");
} catch (error) {
  const message = error instanceof Error ? error.message : String(error);
  console.log(message.startsWith("Expected") || message.includes("JSON"));
}
Output
true

Check with instanceof Error before reading .message.

never: the type with no values#

never means "this cannot happen". It appears in three situations:

Functions that never return#

A function that always throws, or loops forever, has the return type never:

TypeScript
function fail(message: string): never {
  throw new Error(message);
}

try {
  fail("Payment declined");
} catch (error) {
  console.log((error as Error).message);
}
Output
Payment declined

Impossible branches#

When narrowing has ruled out every possibility, the remaining type is never.

Exhaustive checks#

This is the most useful case. Assigning to a never variable makes the compiler tell you when you forget to handle a case:

TypeScript
type Status = "paid" | "pending" | "refunded";

function label(status: Status): string {
  switch (status) {
    case "paid":
      return "Paid";
    case "pending":
      return "Awaiting payment";
    default: {
      const unhandled: never = status;
      return unhandled;
    }
  }
}
Compiler error
error TS2322: Type '"refunded"' is not assignable to type 'never'.

The "refunded" case is missing, so status can still be "refunded" in the default branch, and the error message names exactly that case. Add the missing case and the error disappears. If someone later adds a new status, every switch that forgot it fails to compile. You will use this pattern with Discriminated Unions.

Summary#

TypeAcceptsCan be used without checks?Use for
anyAnythingYes, nothing is checkedTemporary escape hatch only
unknownAnythingNo, narrow firstOutside data, caught errors
neverNothing(no values exist)Functions that never return, exhaustive checks

Practice#

Practice exercise

Write toNumber(value: unknown): number | null that returns numbers as they are, converts numeric strings (after trimming) with Number, and returns null for anything else or for strings that are not numbers.

Show solution
TypeScript
function toNumber(value: unknown): number | null {
  if (typeof value === "number") return Number.isNaN(value) ? null : value;
  if (typeof value === "string" && value.trim() !== "") {
    const n = Number(value.trim());
    return Number.isNaN(n) ? null : n;
  }
  return null;
}

console.log(toNumber(42), toNumber(" 7.5 "), toNumber("abc"), toNumber(true), toNumber(""));
Output
42 7.5 null null null

Key takeaway#

Finished this lesson?