any, unknown, and never are three of the most misunderstood types in TypeScript. The official docs explain what they mean. Production projects show what happens when you pick the wrong one.
This is the practical side: when each one earns its place, and what bug class each one prevents.
any Is The Door Out Of Type Safety
any tells the compiler "I'll handle this myself" — and the compiler immediately stops checking. Any property access works. Any function call works. Any assignment works. Until runtime says no.
function process(data: any) {
data.users.forEach(u => console.log(u.email)); // compiles, may explode
}
If data is actually { users: null }, that line throws at runtime. The compiler had every chance to catch it and chose not to, because you opted out.
The honest place for any is during a migration from JavaScript when you literally don't know the shape yet, or when you're calling into untyped third-party code. Use it as a marker — a temporary placeholder you intend to remove. The minute you have a real shape, replace it.
unknown Is The Right Default For External Data
unknown is the type-safe sibling. It also accepts any value, but it forces you to narrow before you use it. The compiler refuses to let you call .users on an unknown — you have to prove what's inside first.
function process(data: unknown) {
if (
typeof data === 'object' &&
data !== null &&
'users' in data &&
Array.isArray((data as { users: unknown }).users)
) {
// narrowed — safe to use
}
}
That's verbose. In real code you'd use a schema parser (zod, valibot, ajv) to do the narrowing in one call:
import { z } from 'zod';
const Payload = z.object({ users: z.array(z.object({ email: z.string() })) });
function process(data: unknown) {
const parsed = Payload.parse(data); // throws if shape is wrong
parsed.users.forEach(u => console.log(u.email));
}
Use unknown at every boundary where data comes in from outside your code — JSON.parse, localStorage.getItem, message events, query parameters, fetch responses. Inside your code, work with the narrowed types.
never Catches The Cases You Forgot
never is the type with no values. Nothing is assignable to it. That sounds useless until you use it for exhaustive switches:
type PaymentState =
| { status: 'pending' }
| { status: 'paid'; transactionId: string }
| { status: 'failed'; reason: string };
function assertNever(value: never): never {
throw new Error(`Unhandled state: ${JSON.stringify(value)}`);
}
function label(state: PaymentState): string {
switch (state.status) {
case 'pending': return 'Waiting';
case 'paid': return `Paid: ${state.transactionId}`;
case 'failed': return `Failed: ${state.reason}`;
default: return assertNever(state);
}
}
If someone adds 'refunded' to the union later, the default branch suddenly receives a value that is not never — and the compiler refuses to compile until you handle it. The bug that would have shipped silently in JavaScript fails the build instead.
That single pattern — discriminated union plus assertNever — is one of the highest-leverage TypeScript techniques you can adopt. It pays back every time the type evolves.
A Quick Decision Rule
When data crosses into your code from the outside, type it as unknown and parse it. When you genuinely don't know the shape and intend to figure it out later, type it as any and add a TODO. When you've handled every case in a discriminated union, use never to prove it.
If any shows up in your codebase without a TODO, that's not type safety — that's any pretending to be type safety.
Pro Tips
- Default to
unknownoverany. It costs you oneifand saves you from runtime mysteries. - Pair
neverwithassertNeverfor every meaningful discriminated union — refactors become safer. - Forbid
anyin lint config.@typescript-eslint/no-explicit-anywith allowlists for migration files works well. - Watch for implicit
any.noImplicitAny: truecatches the parameters you forgot to type. - Don't suppress narrowing with
as. A type assertion is the same asanyin disguise — it tells the compiler to trust you without proof.
Final Tips
The shortest version: any is a shortcut, unknown is a contract, never is a guarantee. Most TypeScript bugs in production code come from reaching for the first when you wanted the second.
Pick the type that matches the truth, not the one that silences the compiler fastest.
Good luck — and may your any count only go down 👊



