In TypeScript, any switches type checking off for a value, so anything goes and mistakes reach runtime. unknown also accepts any value, but you must narrow it with a check like typeof or a type guard before you can use it, which makes it the safe choice for JSON, API responses and catch variables. never is the opposite end: a type with no possible values, used for code that cannot be reached, functions that always throw, and exhaustive switch checks.
A simple way to remember it: any says "trust me", unknown says "prove it", and never says "this cannot happen". The rest of this guide shows what that means in real code.
Every example was compiled with TypeScript 7.0.2 using --strict and --noEmit, on Node.js v24.22.1. The runtime examples were compiled and then run in Node, and the outputs are copied straight from those runs.
In this guide:
- any: type checking switched off
- How any spreads through your code
- unknown: any value, but you must check it first
- How to narrow unknown safely
- unknown in catch blocks
- never: the type with no values
- A reusable assertNever helper
- Functions that return never
- Types that collapse to never
- The never[] trap with empty arrays
- What can be assigned to what
- any vs unknown vs never compared
- Which one should you use?
- Frequently asked questions
any: type checking switched off
Here is any doing what it does best, hiding two bugs at once:
const data: any = JSON.parse('{"user": {"name": "Ana"}}');
const age: number = data.user.age; // no error, but age is undefined
const upper: string = data.user.name.toUppercase(); // typo, still no error
console.log(age, upper);
[exit 0]
The compiler reported nothing (exit 0), even though age does not exist and toUppercase is a typo for toUpperCase. The bug only shows up when you run it:
a01_any_spreads.js:5
const upper = data.user.name.toUppercase(); // typo, still no error
^
TypeError: data.user.name.toUppercase is not a function
Node.js v24.22.1
This is the same crash my guide to TypeError: x is not a function covers in plain JavaScript. With any, TypeScript cannot catch it for you.
How any spreads through your code
any is contagious. Anything you read from an any value is also any, including function return values:
function getLength(value: any) {
return value.length;
}
const r = getLength(42);
console.log(r);
type R = typeof r;
const check: R = "anything at all";
console.log(check);
[exit 0]
getLength(42) returns any, so typeof r is any, and assigning a random string to it passes. One any parameter can quietly switch off checking for every caller. The typescript-eslint no-explicit-any rule exists to flag this, and many teams turn it on.
A related failure that does not crash is even worse:
const data: any = JSON.parse('{"user": {"name": "Ana"}}');
try {
console.log(data.user.name.toUppercase());
} catch (e) {
console.log(String(e));
}
console.log(data.user.age + 1);
TypeError: data.user.name.toUppercase is not a function
NaN
data.user.age + 1 quietly produced NaN, which then flows into the rest of your program.
unknown: any value, but you must check it first
Change the same code to unknown and the compiler stops you before anything runs:
const data: unknown = JSON.parse('{"user": {"name": "Ana"}}');
console.log(data.user.name);
const n: number = data;
u01_unknown_blocks.ts(2,13): error TS18046: 'data' is of type 'unknown'.
u01_unknown_blocks.ts(3,7): error TS2322: Type 'unknown' is not assignable to type 'number'.
TS18046: 'data' is of type 'unknown' means you tried to use a value before proving what it is. You also cannot assign unknown to a number without a check. The TypeScript 3.0 release notes, where unknown was introduced, describe it as the type-safe counterpart of any.
How to narrow unknown safely
You turn unknown into a usable type with checks the compiler understands: typeof, Array.isArray, instanceof, in, or a custom type guard that returns value is User:
type User = { name: string; age?: number };
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof (value as { name: unknown }).name === "string"
);
}
const data: unknown = JSON.parse('{"name": "Ana"}');
if (isUser(data)) {
console.log(data.name.toUpperCase());
} else {
console.log("not a user");
}
function describe(x: unknown): string {
if (typeof x === "string") return `string of length ${x.length}`;
if (typeof x === "number") return `number ${x.toFixed(2)}`;
if (Array.isArray(x)) return `array with ${x.length} items`;
return "something else";
}
console.log(describe("hi"), "|", describe(3.14159), "|", describe([1, 2]), "|", describe(null));
[exit 0]
ANA
string of length 2 | number 3.14 | array with 2 items | something else
Inside each if, TypeScript knows the exact type, so x.toFixed(2) and data.name.toUpperCase() are fully checked. The handbook chapter on narrowing lists every check that works this way. For larger API payloads, a schema library such as Zod does the same job with less code.
unknown in catch blocks
Since TypeScript 4.4, the useUnknownInCatchVariables option makes catch variables unknown instead of any, and --strict turns it on. That is why this common line fails:
try {
JSON.parse("{bad json}");
} catch (err) {
console.log(err.message);
}
u03_catch.ts(4,15): error TS18046: 'err' is of type 'unknown'.
JavaScript lets you throw anything, including a string or a number, so TypeScript cannot promise that err has a message. Check it with instanceof Error:
try {
JSON.parse("{bad json}");
} catch (err) {
if (err instanceof Error) {
console.log(err.name + ": " + err.message);
} else {
console.log("non-Error thrown:", err);
}
}
[exit 0]
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
The tsconfig reference for useUnknownInCatchVariables explains the option and how to switch it off, though leaving it on is the safer choice.
never: the type with no values
never describes something that can never happen. You rarely write it on a variable, but it appears in three useful places: exhaustive checks, functions that never return, and types that reduce to nothing.
The most practical use is making the compiler tell you when you forget a case. Here a triangle was added to Shape, but not to the switch:
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.r ** 2;
case "square":
return s.side ** 2;
default: {
const unhandled: never = s;
throw new Error(`Unhandled shape: ${JSON.stringify(unhandled)}`);
}
}
}
n01_never_exhaustive.ts(13,13): error TS2322: Type '{ kind: "triangle"; base: number; height: number; }' is not assignable to type 'never'.
In the default branch, s should have been narrowed to never, because all cases were handled. The triangle was left over, so the assignment failed and the error names the exact missing case. The handbook's narrowing chapter calls this exhaustiveness checking, and it is the best reason to know never.
A reusable assertNever helper
Wrap the check in a function, add the missing case, and the code compiles and runs:
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };
function assertNever(x: never): never {
throw new Error(`Unhandled value: ${JSON.stringify(x)}`);
}
function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.r ** 2;
case "square":
return s.side ** 2;
case "triangle":
return (s.base * s.height) / 2;
default:
return assertNever(s);
}
}
console.log(area({ kind: "square", side: 3 }), area({ kind: "triangle", base: 4, height: 5 }));
[exit 0]
9 10
Now every time someone adds a new shape, each switch that uses assertNever turns red until it handles it. That is much cheaper than finding the gap in production.
Functions that return never
A function that always throws, or loops forever, has the return type never. TypeScript then knows the code after the call cannot run:
function fail(message: string): never {
throw new Error(message);
}
function parsePort(value: string): number {
const n = Number(value);
if (Number.isInteger(n) && n > 0 && n < 65536) return n;
return fail(`Invalid port: ${value}`);
}
console.log(parsePort("8080"));
try {
parsePort("abc");
} catch (e) {
console.log((e as Error).message);
}
[exit 0]
8080
Invalid port: abc
Because fail() returns never, return fail(...) is accepted in a function that must return a number. The handbook page on functions covers never along with unknown and void.
Types that collapse to never
Some type expressions leave no possible values, and TypeScript simplifies them to never:
type A = string & number;
type B = Exclude<"a" | "b" | "c", "a" | "b" | "c">;
type C = Extract<"x" | 1 | true, string>;
const a: A = "hello";
const b: B = "a";
const c: C = "x";
n04_never_intersection.ts(4,7): error TS2322: Type '"hello"' is not assignable to type 'never'.
n04_never_intersection.ts(5,7): error TS2322: Type '"a"' is not assignable to type 'never'.
Nothing is both a string and a number, and excluding every member of a union leaves nothing. Extract kept "x", so line 6 passed. If you see not assignable to type 'never' in your own code, look for an intersection or a filter that removed everything. The utility types reference documents Exclude and Extract.
The never[] trap with empty arrays
The most common way people meet never by accident is an empty array inside an object:
const state = { items: [] };
state.items.push("apple");
n06_never_object_array.ts(2,18): error TS2345: Argument of type '"apple"' is not assignable to parameter of type 'never'.
With nothing to infer from, TypeScript typed items as never[], so nothing can be pushed into it. A bare const items = [] behaves better under --strict, because noImplicitAny lets the array "evolve" as you push. Turn noImplicitAny off and even that case breaks:
const items = [];
items.push("apple");
const ids = [] as never[];
ids.push(1);
n03_never_empty_array.ts(2,12): error TS2345: Argument of type '"apple"' is not assignable to parameter of type 'never'.
n03_never_empty_array.ts(4,10): error TS2345: Argument of type '1' is not assignable to parameter of type 'never'.
The fix is to give the empty array a type:
const state: { items: string[] } = { items: [] };
state.items.push("apple");
const state2 = { items: [] as string[] };
state2.items.push("pear");
console.log(state.items, state2.items);
[exit 0]
[ 'apple' ] [ 'pear' ]
The same thing happens with useState([]) in React. I checked it with @types/react 19:
import { useState } from "react";
export function TodoList() {
const [items, setItems] = useState([]);
const [names, setNames] = useState<string[]>([]);
setItems(["apple"]);
setNames(["apple"]);
return items.length + names.length;
}
r01_usestate.tsx(6,13): error TS2322: Type 'string' is not assignable to type 'never'.
Only setItems failed; the typed useState<string[]>([]) version passed. My guide to Type 'X' is not assignable to type 'Y' covers more errors of this family.
What can be assigned to what
This file sums up the rules. Each line either compiles or fails, and the comments describe the direction:
declare const anyValue: any;
declare const unknownValue: unknown;
declare const neverValue: never;
// Everything can go INTO any and unknown
const a1: any = 42;
const u1: unknown = 42;
// any can go anywhere, unknown only into any/unknown
const s1: string = anyValue;
const s2: string = unknownValue;
const s3: unknown = unknownValue;
// never can go anywhere, nothing goes into never
const s4: string = neverValue;
const n1: never = 42;
x01_assignability.ts(11,7): error TS2322: Type 'unknown' is not assignable to type 'string'.
x01_assignability.ts(16,7): error TS2322: Type '42' is not assignable to type 'never'.
Only two lines failed: unknown into string, and 42 into never. Everything else, including any into string and never into string, passed. In type theory terms, unknown is the top type (everything fits into it) and never is the bottom type (it fits into everything). any breaks the rules in both directions, which is why it is unsafe. Note that unknown also covers null and undefined, so a type guard should rule them out; my post on null vs undefined explains the difference.
any vs unknown vs never compared
| any | unknown | never |
|---|
| What it means | Turn type checking off | Some value, type not known yet | No value can exist |
| Can you assign anything to it? | Yes | Yes | No (only never) |
| Can you assign it to other types? | Yes, to anything | Only to unknown and any | Yes, to anything |
| Can you use properties and methods? | Yes, unchecked | No, narrow first (TS18046) | Not reachable |
| Typical use | Migrating old JS, quick escape hatch | JSON, API data, catch variables | Exhaustive switch, functions that always throw |
| Safety | None | Full | Full |
The everyday types chapter of the handbook also recommends noImplicitAny, which --strict enables, so that any only appears where you wrote it on purpose.
Which one should you use?
- Data from outside your code (JSON,
fetch(), localStorage, user input, catch): use unknown and narrow it. - You know the shape: write a real type or interface. My comparison of interface vs type helps you pick.
- A case that must never happen: use
never with an assertNever helper. - Migrating a JavaScript file or working around a broken library type:
any is acceptable, but keep it local and leave a comment explaining why.
If you are unsure, start with unknown. Moving from unknown to a real type is easy, while removing any from a codebase later is slow work.
Frequently asked questions
What is the difference between any and unknown in TypeScript?
Both accept any value. With any you can use the value however you like and TypeScript does not check it. With unknown you must narrow the type first, for example with typeof, before you can use it.
When should I use never in TypeScript?
Use never for exhaustive switch checks, for functions that always throw or never return, and to describe impossible types. You rarely annotate a normal variable with never.
What does 'is of type unknown' (TS18046) mean?
You tried to read a property or call a method on a value typed unknown. Narrow it first with typeof, instanceof, Array.isArray, an in check or a type guard.
Why is my catch error of type unknown?
Under --strict, the useUnknownInCatchVariables option makes catch variables unknown, because JavaScript can throw any value. Use if (err instanceof Error) before reading err.message.
Why do I get not assignable to parameter of type 'never' when pushing to an array?
The array was created empty without a type, often inside an object or useState, so TypeScript inferred never[]. Give it a type, such as items: string[] or useState<string[]>([]).
Is unknown better than any?
For values whose type you do not know, yes. unknown keeps type checking on and forces a check before use, while any turns checking off and lets bugs reach runtime.
Can I assign never to a string?
Yes. never is assignable to every type because a never value cannot exist. Nothing except never itself can be assigned to never.
Last checked: 9 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.