TypeScript 18 ๐ท Unknown, any, and never
TypeScript has three types that sit outside the normal type hierarchy: unknown, any, and never. They’re the top, middle, and bottom of the type system โ the three extremes. unknown is the top type โ everything is assignable to it, but you can’t use it without narrowing. any is the escape hatch โ everything is assignable to it and it’s assignable to everything. never is the bottom type โ nothing is assignable to it, and it’s assignable to everything. Each has a purpose. Mixing them up causes bugs.
Key point: unknown is safe and narrow. any is unsafe and wide. never is empty and perfect for exhaustiveness. The order of preference is unknown first, never for exhaustiveness, and any only when there’s no alternative. Almost every use of any in a codebase should be unknown instead.
The three extremes
The type system has a top, a bottom, and a trap door.
| Type | Assignable to it | Assignable from it | Safe? |
|---|---|---|---|
unknown | Everything | Nothing without narrowing | โ |
any | Everything | Everything | โ |
never | Nothing | Everything | โ |
Reading the table:
unknownaccepts any value, but you can’t use it until you narrowanyaccepts any value and can be used as anything โ no checksneveraccepts no values, but can be used anywhere
Where each sits:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ unknown (top) โ
โ โ โ
โ โ every type is โ
โ โ assignable to it โ
โ โผ โ
โ โโโโโโโโโโโโ โ
โ โ any โ (both directions) โ
โ โโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ never (bottom) โ
โ every type โ
โ is assignable โ
โ from it โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Why three: They serve different purposes. unknown is for values whose type you don’t know yet. any is for when TypeScript’s type system can’t help. never is for values that don’t exist โ unreachable code, empty unions, impossible types.
Why they exist: A type system needs a top and a bottom to be complete.
unknownis the top โ the widest possible type.neveris the bottom โ the narrowest.anyis a non-type, an escape from the system itself. Understanding where each fits makes you deliberate about using them.
unknown โ the safe top
unknown is the top type. Any value is assignable to unknown, but you can’t use unknown as anything until you narrow it.
let value: unknown = 42;
value = 'hello'; // โ
any type is assignable
value = { id: 1 }; // โ
value.toUpperCase(); // โ can't use unknown
if (typeof value === 'string') {
value.toUpperCase(); // โ
narrowed
}
The rules:
- Everything is assignable to
unknown unknownis assignable only tounknownorany- You must narrow before using the value
Where unknown shines:
- External data โ
JSON.parse,fetch().json(), user input, file contents - API boundaries โ values from untyped or loosely typed libraries
- Public function parameters โ when the function accepts anything but must validate
- Catch variables โ
catch (e)underuseUnknownInCatchVariables
JSON.parse returns any by default โ but you can treat it as unknown:
const data: unknown = JSON.parse(input);
// Now you must validate before use
if (isUser(data)) {
console.log(data.name); // โ
}
Catch variables:
try {
doThing();
} catch (err) {
// err is unknown under strict
if (err instanceof Error) {
console.error(err.message); // โ
} else {
console.error('Unknown error');
}
}
useUnknownInCatchVariables (part of strict) makes err unknown instead of any. This is a significant safety improvement โ you’re forced to handle errors properly.
unknown in functions:
function handle(value: unknown): void {
if (typeof value === 'number') {
// value is number
} else if (typeof value === 'string') {
// value is string
}
// value is unknown here
}
The narrowing toolkit works on unknown โ typeof, instanceof, in, predicates.
Why unknown is safe: You can’t accidentally use it. Every read requires narrowing. That’s the entire point โ it forces the boundary between untyped data and typed code.
Why
unknownis the “safe any”: It has the same acceptance range โ anything can be assigned โ but it can’t be used without narrowing. Whereanysilently lets you write code that crashes at runtime,unknownforces you to handle the types explicitly. It’s the type-safe version of a value you don’t know yet.
any โ the escape hatch
any is a non-type. It’s assignable to and from everything, and TypeScript turns off checking for anything involving any.
let value: any = 42;
value = 'hello'; // โ
value = { id: 1 }; // โ
value.toUpperCase(); // โ
no check โ crashes at runtime if not a string
value.foo.bar.baz(); // โ
no check โ probably crashes
Why any is dangerous:
- It disables type checking
- It propagates โ anything touching
anybecomesany - It hides real bugs
- It makes refactoring unsafe
any propagation:
const a: any = { id: 1 };
const b = a.id; // b is any
const c = b + 1; // c is any
const d = c.toUpperCase(); // d is any โ no error, will crash
Once a value is any, its downstream uses are unchecked. That spread is why any is so damaging โ a single any can disable type checking across a large section of code.
Where any is legitimately used:
- Migrating JavaScript code
- Interop with libraries that have no types
- Quick prototypes
- TypeScript’s own internal types
Where any is not needed:
- Where
unknownwill work - Where a proper type exists
- Where a generic parameter would do
- To silence compiler errors
noImplicitAny (part of strict) prevents implicit any:
function f(x) { } // โ implicit any under strict
function g(x: number) { } // โ
Explicit any still works, but implicit any โ where the compiler would have inferred any because it couldn’t infer anything else โ is rejected.
Alternatives to any:
| Need | Better option |
|---|---|
| Unknown external data | unknown |
| “Anything works here” | unknown + validation |
| Generic flexibility | Type parameter <T> |
| JSON output | unknown then validate |
| Arbitrary object | Record<string, unknown> |
| Library without types | unknown + custom .d.ts |
| Silencing errors | Fix the type |
Why
anyis not banned: TypeScript’s design team keptanybecause some scenarios genuinely need it โ during migrations, when bridging untyped code. But the guidance is clear: preferunknownin almost every case.anyis a last resort, not a tool.
never โ the empty bottom
never is the bottom type. Nothing is assignable to it, and it’s assignable to everything.
let x: never;
x = 1; // โ
x = 'a'; // โ
// Nothing can be assigned to never.
Where never appears:
- Functions that never return โ
throwor infinite loop - Exhaustiveness checks โ the remainder after handling all cases
- Impossible intersections โ
string & number - Conditional type filters โ mapping a case to
never
Function returns:
function fail(msg: string): never {
throw new Error(msg);
}
Exhaustiveness:
function assertNever(x: never): never {
throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}
function handle(status: 'a' | 'b' | 'c'): string {
switch (status) {
case 'a': return 'A';
case 'b': return 'B';
case 'c': return 'C';
default: return assertNever(status); // status is never here
}
}
Impossible types:
type Empty = string & number; // never
Filtering:
type NonNullish<T> = T extends null | undefined ? never : T;
Why never matters: It’s how TypeScript proves exhaustiveness, marks unreachable code, and filters unions. Without it, exhaustiveness checking wouldn’t exist, and conditional types couldn’t express “this case disappears.”
Why the bottom type is useful: Type theory needs a bottom โ a type with no instances. In TypeScript, that’s
never. It represents the impossible: a value that can’t exist, code that can’t run, a union with no members. That emptiness is what enables exhaustiveness checks and unreachable-code detection.
The three types compared
A side-by-side view.
| Property | unknown | any | never |
|---|---|---|---|
| Top of hierarchy | โ | โ | โ |
| Bottom of hierarchy | โ | โ | โ |
| Assignable to it | Everything | Everything | Nothing |
| Assignable from it | Nothing without narrowing | Everything | Everything |
| Usable without narrowing | โ | โ | N/A (no values) |
| Type checking | Full (after narrowing) | Disabled | Full |
| Safe? | โ | โ | โ |
| Common use | External data | Escape hatch | Exhaustiveness |
Examples:
// unknown โ safe but requires narrowing
function f1(x: unknown): string {
if (typeof x === 'string') return x;
return String(x);
}
// any โ unsafe but no restrictions
function f2(x: any): string {
return x.toUpperCase(); // no check, may crash
}
// never โ no values, only return position
function f3(): never {
throw new Error();
}
How each behaves with assignment:
| Expression | Result |
|---|---|
const x: unknown = 42 | โ |
const x: unknown = 'hi' | โ |
const x: any = 42 | โ |
const x: any = 'hi' | โ |
const x: never = 42 | โ |
const x: string = unknownValue | โ |
const x: string = anyValue | โ |
const x: string = neverValue | โ |
How each interacts with function parameters:
| Signature | Accepts |
|---|---|
(x: unknown) => void | Any value |
(x: any) => void | Any value |
(x: never) => void | Nothing โ rarely called |
A never parameter effectively makes a function uncallable from typical code โ useful for exhaustiveness helpers.
Why the comparison matters: They look similar but behave oppositely.
unknownforces you to check;anydisables checking;nevercan’t hold anything. Knowing which to reach for is the difference between safe code and silent bugs. The rule:unknownby default,neverfor exhaustiveness,anynever.
Choosing between them
The decision tree is simple.
Use unknown when:
- Receiving external data (JSON, API, user input, files)
- A function accepts anything but must validate
- You’re unsure of the type and want safety
- Catching errors (catch variable)
- Typing library interop you don’t fully trust
Use any when:
- Migrating legacy code and need a temporary shortcut
- Interfacing with untyped third-party code you can’t fix
- Prototyping and will type later
- Never as a permanent design choice
Use never when:
- Exhaustiveness checking
- Functions that never return
- Filtering union members with conditional types
- Signalling impossible branches
- Bottom type operations
The rule of thumb:
Prefer unknown.
Use never for exhaustiveness.
Avoid any unless there's no alternative.
Common scenarios:
| Scenario | Type |
|---|---|
JSON.parse result | unknown |
fetch().json() result | unknown |
| Catch variable | unknown |
| Event handler with unknown shape | unknown |
| Function returns from throw | never |
| Exhaustive switch default | never |
| Legacy library | Try unknown first |
Migration path: When you find any in your code, ask:
- Can this be
unknownwith validation? โ Replace - Can this be a proper type? โ Replace
- Can this be generic? โ Replace
- Is
anygenuinely required? โ Keep, with a comment
Most any uses become unknown with a guard or a proper type.
Why the preference is clear:
unknownprovides the same flexibility asanyโ accept anything โ but requires explicit handling. That requirement is exactly what you want at boundaries.anylets you write code that type-checks but crashes.unknownforces the check that prevents the crash. There’s almost never a reason to pickanyoverunknownin new code.
Type narrowing from unknown
The narrowing toolkit works on unknown โ it’s how you get from “unknown value” to “typed value.”
typeof:
function f(x: unknown): string {
if (typeof x === 'string') return x.toUpperCase();
if (typeof x === 'number') return x.toFixed(2);
if (typeof x === 'boolean') return x ? 'yes' : 'no';
return 'unknown';
}
instanceof:
function f(x: unknown): string {
if (x instanceof Error) return x.message;
if (x instanceof Date) return x.toISOString();
return 'unknown';
}
in:
function f(x: unknown): string {
if (typeof x === 'object' && x !== null && 'name' in x) {
return String((x as { name: unknown }).name);
}
return 'unknown';
}
Type predicates:
function isUser(x: unknown): x is User {
return (
typeof x === 'object' &&
x !== null &&
'id' in x &&
typeof (x as { id: unknown }).id === 'number' &&
'name' in x &&
typeof (x as { name: unknown }).name === 'string'
);
}
function handle(x: unknown): void {
if (isUser(x)) {
console.log(x.name); // โ
x is User
}
}
Array.isArray:
function f(x: unknown): string {
if (Array.isArray(x)) {
return `Array of ${x.length}`;
}
return 'not array';
}
Validation libraries: Use Zod, io-ts, Valibot, or similar for complex shapes.
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string()
});
const parsed = UserSchema.safeParse(data);
if (parsed.success) {
parsed.data.name; // โ
typed
}
Why narrowing from unknown is the norm: External data starts as unknown. The way to typed data is narrowing โ typeof, instanceof, in, predicates, or a validation library. Once narrowed, the rest of the code works on a specific type.
Why narrowing is required:
unknowncan hold anything โ so TypeScript refuses to let you use it as anything specific. Narrowing is the process of proving the type at runtime. Once proven, TypeScript trusts the narrowed type and lets you use it. It’s the type-safe counterpart to JavaScript’s loose runtime checks.
The any trap
any doesn’t just disable checks for one value โ it spreads.
any from a library:
import something from 'untyped-lib'; // something: any
const value = something.get(); // value: any
const result = value.foo.bar; // result: any
result.nonexistent.method(); // โ no error โ crashes at runtime
One any from an import taints everything downstream.
any in a generic:
function wrap<T>(value: T): T {
return value;
}
const a = wrap(anything as any); // a: any
const b = a.toUpperCase(); // b: any โ no check
any in JSON:
const data = JSON.parse(input); // any
data.user.name.toUpperCase(); // โ
compiles โ may crash
If data.user is undefined, the code crashes. With unknown, you’d need to narrow first.
any from callbacks:
const fn = (x: any) => x.toUpperCase();
[1, 2, 3].map(fn); // no error โ crashes
How to detect any in your code:
- Enable
noImplicitAny(strict) - ESLint rule
@typescript-eslint/no-explicit-any - Search for
: anyandas any - Use
unknownin every location whereanyappeared
How to replace any:
// Before
function process(data: any) {
return data.value;
}
// After
function process(data: unknown): string {
if (typeof data === 'object' && data !== null && 'value' in data) {
return String((data as { value: unknown }).value);
}
throw new Error('Invalid data');
}
The unknown version is longer but safe. It documents the shape and handles the failure case.
When any is truly unavoidable: Comment why. A code reviewer seeing any should immediately understand the reason โ usually a library’s bad types.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const legacyResult: any = legacyLib.getResult();
Why
anyspreads: TypeScript’s type checker treatsanyas a wildcard. Any operation on ananyproduces anotherany. That cascading effect is why a singleanycan hide bugs across a whole module.unknowndoesn’t spread โ every operation onunknownrequires narrowing, which produces a specific type.
A full example
A boundary handler that uses unknown to validate external data.
// ============================================
// TYPES
// ============================================
interface User {
id: number;
name: string;
email: string;
}
// ============================================
// PREDICATE
// ============================================
function isUser(x: unknown): x is User {
return (
typeof x === 'object' &&
x !== null &&
'id' in x &&
typeof (x as { id: unknown }).id === 'number' &&
'name' in x &&
typeof (x as { name: unknown }).name === 'string' &&
'email' in x &&
typeof (x as { email: unknown }).email === 'string'
);
}
// ============================================
// BOUNDARY
// ============================================
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/users/${id}`);
const data: unknown = await res.json(); // unknown, not any
if (!isUser(data)) {
throw new Error('Invalid user payload');
}
return data; // โ
narrowed to User
}
// ============================================
// ERROR HANDLING
// ============================================
function describeError(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === 'string') return err;
return 'Unknown error';
}
// ============================================
// NEVER โ EXHAUSTIVE
// ============================================
function assertNever(x: never): never {
throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}
type Status = 'idle' | 'loading' | 'ready' | 'error';
function label(s: Status): string {
switch (s) {
case 'idle': return 'Waiting';
case 'loading': return 'Loading';
case 'ready': return 'Ready';
case 'error': return 'Failed';
default: return assertNever(s);
}
}
// ============================================
// USAGE
// ============================================
async function main(): Promise<void> {
try {
const user = await fetchUser(1);
console.log(user.name);
} catch (err) {
console.error(describeError(err));
}
}
console.log(label('idle'), label('ready'));
Every type at the boundary is unknown. Every function that throws or exhaustively checks returns never. No any anywhere.
What this demonstrates:
unknownfor the JSON response- A predicate narrows to
User unknownfor the catch variableneverfor the throwing helperassertNeverfor exhaustiveness
Why this shape: It’s how real boundary code should look. External data is
unknownuntil validated. Errors areunknownuntil narrowed. Nothing isany. The type system protects the entire pipeline from a crash caused by incorrect assumptions about external data.
Complete Example Session
# ============================================
# PART 1: UNKNOWN BASICS
# ============================================
cat > unknown.ts << 'EOF'
let value: unknown = 42;
value = 'hello';
value = { id: 1 };
// value.toUpperCase(); // โ not allowed
if (typeof value === 'object' && value !== null) {
console.log(Object.keys(value));
}
function handle(x: unknown): string {
if (typeof x === 'string') return x.toUpperCase();
if (typeof x === 'number') return x.toFixed(2);
if (x instanceof Date) return x.toISOString();
return 'unknown';
}
console.log(handle('hi'), handle(42), handle(new Date()), handle(null));
EOF
npx tsc --noEmit unknown.ts
# (no errors)
# ============================================
# PART 2: ANY PROPAGATION
# ============================================
cat > any.ts << 'EOF'
const data: any = { user: { name: 'Alice' } };
// No errors โ but crashes at runtime if shape is wrong
console.log(data.user.name.toUpperCase());
console.log(data.missing.deep.value); // โ
compiles, โ crashes
// any propagates
const a = data.foo.bar.baz; // a: any
console.log(a.whatever()); // โ
compiles, crashes
EOF
npx tsc --noEmit any.ts
# (no errors โ that's the problem)
# ============================================
# PART 3: NEVER AND EXHAUSTIVENESS
# ============================================
cat > never.ts << 'EOF'
function assertNever(x: never): never {
throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}
type Status = 'idle' | 'loading' | 'ready';
function message(s: Status): string {
switch (s) {
case 'idle': return 'Waiting';
case 'loading': return 'Loading';
case 'ready': return 'Ready';
default: return assertNever(s);
}
}
function fail(msg: string): never {
throw new Error(msg);
}
console.log(message('idle'));
EOF
npx tsc --noEmit never.ts
# (no errors)
# ============================================
# PART 4: NEVER FAILS ON MISSING CASE
# ============================================
cat > missing.ts << 'EOF'
function assertNever(x: never): never { throw x; }
type Status = 'a' | 'b' | 'c';
function f(s: Status): string {
switch (s) {
case 'a': return 'A';
case 'b': return 'B';
// โ 'c' missing
default: return assertNever(s);
}
}
EOF
npx tsc --noEmit missing.ts
# [ missing.ts:9:32 - Argument of type '"c"' is not assignable to parameter of type 'never'. ]
rm missing.ts
# ============================================
# PART 5: UNKNOWN AT BOUNDARIES
# ============================================
cat > boundary.ts << 'EOF'
interface User {
id: number;
name: string;
}
function isUser(x: unknown): x is User {
return (
typeof x === 'object' &&
x !== null &&
'id' in x && typeof (x as { id: unknown }).id === 'number' &&
'name' in x && typeof (x as { name: unknown }).name === 'string'
);
}
const raw: unknown = JSON.parse('{"id":1,"name":"Alice"}');
if (isUser(raw)) {
console.log(raw.name); // โ
narrowed
} else {
console.log('not a user');
}
// Catch
try {
throw 'oops';
} catch (err) {
if (err instanceof Error) console.log(err.message);
else if (typeof err === 'string') console.log(err);
}
EOF
npx tsc --noEmit boundary.ts
# (no errors)
# ============================================
# PART 6: NEVER IN TYPES
# ============================================
cat > never-types.ts << 'EOF'
type Empty = string & number; // never
type NonNullish<T> = T extends null | undefined ? never : T;
type A = NonNullish<string | null>; // string
type B = NonNullish<number | undefined>; // number
type Union = string | never; // string
// @ts-expect-error - empty intersection can't hold a value
const x: Empty = 42;
console.log({} as A extends string ? 'string' : 'other');
EOF
npx tsc --noEmit never-types.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc unknown.ts never.ts boundary.ts never-types.ts
node unknown.js
# [ HI 42 2024-... unknown ]
node never.js
# [ Waiting ]
node boundary.js
# [ Alice ]
# [ oops ]
node never-types.js
# [ string ]
Quick Reference
The Three Types
| Property | unknown | any | never |
|---|---|---|---|
| Position | Top | Escape | Bottom |
| Assignable to it | Anything | Anything | Nothing |
| Assignable from it | Nothing (without narrowing) | Anything | Anything |
| Safe | โ | โ | โ |
| Purpose | External data | Escape hatch | Exhaustiveness |
Assignability
| From โ To | unknown | any | never |
|---|---|---|---|
unknown โ string | โ | โ | โ |
any โ string | โ | โ | โ |
never โ string | โ | โ | โ |
string โ unknown | โ | โ | โ |
string โ any | โ | โ | โ |
string โ never | โ | โ | โ |
When to Use Each
| Use case | Type |
|---|---|
| External JSON | unknown |
| Catch variable | unknown |
| API response | unknown |
| User input | unknown |
| Untyped library | unknown first |
| Function that throws | never |
| Exhaustive switch default | never |
| Impossible intersection | never |
| Legacy migration | any (temporarily) |
| Absolutely no alternative | any (with comment) |
Narrowing from unknown
| Tool | Example |
|---|---|
typeof | typeof x === 'string' |
instanceof | x instanceof Error |
in | 'name' in x |
Array.isArray | Array.isArray(x) |
| Predicate | isUser(x): x is User |
| Library | Schema.safeParse(x) |
unknown in Function Signatures
| Signature | Accepts |
|---|---|
(x: unknown) => void | Anything |
(x: any) => void | Anything |
(x: never) => void | Nothing |
any Propagation
| Operation | Result type |
|---|---|
const x: any | any |
x.foo | any |
x.foo.bar | any |
x() | any |
x + 1 | any |
[x] | any[] |
{ a: x } | { a: any } |
never Appearances
| Context | Example |
|---|---|
| Function return | function f(): never { throw } |
| Exhaustiveness | After all cases |
| Impossible type | string & number |
| Conditional filter | T extends X ? never : T |
| Union identity | A | never = A |
| Intersection absorbing | A & never = never |
| Unreachable code | After return/throw |
Common Patterns
| Pattern | Type |
|---|---|
| Boundary validation | unknown + predicate |
| Exhaustiveness | never + assertNever |
| Throwing helper | never return |
| Error narrowing | unknown in catch |
| Filter nullish | T extends null ? never : T |
Anti-Patterns
| Pattern | Problem |
|---|---|
: any as default | Disables checks |
as any to silence errors | Hides bugs |
JSON.parse() as Foo | No validation |
catch (e: any) | Unchecked errors |
Returning any from functions | Spreads to callers |
never as function parameter | Uncallable |
strict Flags Related
| Flag | Effect |
|---|---|
noImplicitAny | Reject implicit any |
useUnknownInCatchVariables | Catch is unknown |
strictNullChecks | Related null safety |
Migration Cheatsheet
| Before | After |
|---|---|
any | unknown + predicate |
any | Type parameter |
any | Proper interface |
catch (e: any) | catch (e) โ unknown |
JSON.parse(x) as T | unknown + validate |
any[] | unknown[] + filter |
Best Practices
โ Do This:
// Use unknown for external data
const data: unknown = JSON.parse(input); // โ
// Narrow before use
if (isUser(data)) console.log(data.name); // โ
// Use unknown in catch
try { ... } catch (err) {
if (err instanceof Error) log(err.message); // โ
}
// Use never for exhaustiveness
default: return assertNever(x); // โ
// Use never for throwing functions
function fail(msg: string): never { throw new Error(msg); } // โ
// Use unknown in function signatures when accepting anything
function handle(x: unknown): void { /* narrow */ } // โ
// Validate at boundaries with predicates or libraries
const parsed = Schema.safeParse(x); // โ
// Comment deliberate any usage
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const legacy: any = oldLib.get(); // โ
โ Don’t Do This:
// Don't use any as a shortcut
function f(x: any) { return x.toUpperCase(); } // โ
// Don't use `as any` to silence errors
const x = thing as any; // โ
// Don't cast JSON.parse results without validation
const user = JSON.parse(raw) as User; // โ ๏ธ unvalidated
// Don't catch with any
try { } catch (e: any) { } // โ
// Don't use never as a parameter type
function f(x: never): void { } // โ ๏ธ uncallable
// Don't return any from public functions
function get(): any { return this.data; } // โ
// Don't use `any` in generics
function wrap<T = any>(x: T) { } // โ ๏ธ use unknown
// Don't forget to narrow unknown
const x: unknown = ...;
x.foo; // โ not allowed
// Don't mistake never for unreachable
function f(): never { return; } // โ error
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
any as default type | Disables checks | Use unknown |
as any to silence | Hides bugs | Fix the type |
No strict | Many implicit anys | Enable strict |
JSON.parse() as T | No validation | Validate with predicate |
Returning any | Spreads to callers | Return specific type |
never as parameter | Uncallable | Use only for exhaustiveness helpers |
Confusing unknown and any | Different safety | unknown requires narrowing |
Not narrowing unknown | Compile error | Use typeof/instanceof |
Empty catch block | Swallows errors | Handle the error |
any in generics | Defeats generic | Use unknown |
Real-World Examples
1. JSON parse result
const data: unknown = JSON.parse(input);
2. Fetch response
const json: unknown = await res.json();
3. Catch variable
try { } catch (err) {
if (err instanceof Error) log(err.message);
}
4. Predicate narrowing
function isUser(x: unknown): x is User { /* ... */ }
5. Exhaustive check
default: return assertNever(x);
6. Throwing helper
function fail(msg: string): never { throw new Error(msg); }
7. Filter with never
type NonNullish<T> = T extends null ? never : T;
8. Impossible type
type Empty = string & number; // never
9. Zod validation
const parsed = Schema.safeParse(raw);
if (parsed.success) parsed.data.name;
10. Array narrowing
if (Array.isArray(x)) x.map(...);
11. Object narrowing
if (typeof x === 'object' && x !== null) Object.keys(x);
12. Function accepting anything
function log(x: unknown): void { console.log(x); }
13. Reducer exhaustiveness
switch (action.type) {
case 'ADD': return add(...);
default: return assertNever(action);
}
14. Error describe
function describe(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === 'string') return err;
return 'Unknown';
}
15. URLSearchParams value
const value: string | null = params.get('q');
16. Promise resolve value
const v: unknown = await promise;
17. Discriminated result
type Result = { ok: true; value: T } | { ok: false; error: string };
18. Never in default
default: {
const _: never = x;
throw new Error('unreachable');
}
19. Unknown array
function isStringArray(x: unknown): x is string[] {
return Array.isArray(x) && x.every(v => typeof v === 'string');
}
20. Deliberate any
// Legacy interop โ typed badly upstream
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const legacy: any = oldLib.getValue();
Visual: The Type Hierarchy
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ unknown โ
โ โฑ โฒ โ
โ string number boolean ... โ
โ โฒ โฑ โ
โ never โ
โ โ
โ every type is assignable to unknown โ
โ never is assignable to every type โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ any โ outside the hierarchy โ
โ โ
โ โ assignable to everything โ
โ โ everything assignable to it โ
โ โ type checking disabled โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: unknown vs any
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ unknown โ
โ โ
โ const x: unknown = 'hello'; โ
โ โ
โ x.toUpperCase(); โ must narrow โ
โ โ
โ if (typeof x === 'string') { โ
โ x.toUpperCase(); โ
โ
โ } โ
โ โ
โ Safe but requires work โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ any โ
โ โ
โ const x: any = 'hello'; โ
โ โ
โ x.toUpperCase(); โ
no check โ
โ x.foo.bar(); โ
no check โ
โ โ
โ Fast to write โ crashes at runtime โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: never in Exhaustiveness
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ type Status = 'a' | 'b' | 'c' โ
โ โ
โ switch (s) { โ
โ case 'a': ... โ
โ case 'b': ... โ
โ case 'c': ... โ
โ default: โ
โ // s: never โ
โ assertNever(s); โ
โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Missing one case: โ
โ โ
โ switch (s) { โ
โ case 'a': ... โ
โ case 'b': ... โ
โ default: โ
โ // s: 'c' โ
โ assertNever(s); โ compile error โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: any Propagation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const x: any = getData(); โ
โ โ โ
โ โผ โ
โ x.user โ any โ
โ โ โ
โ โผ โ
โ x.user.name โ any โ
โ โ โ
โ โผ โ
โ x.user.name.toUpperCase() โ any โ
โ โ
โ Every step unchecked โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const x: unknown = getData(); โ
โ โ โ
โ โผ โ
โ typeof check required before use โ
โ โ โ
โ โผ โ
โ Narrowed to specific type โ
โ โ
โ Every step checked โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: When to Use Each
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ External data? โ unknown โ
โ Catch variable? โ unknown โ
โ Truly anything accepted? โ unknown โ
โ Exhaustive check? โ never โ
โ Function throws? โ never โ
โ Impossible type? โ never โ
โ Legacy migration? โ any (temp) โ
โ Untyped library? โ unknown first โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Type Flow at a Boundary
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ External (untyped) โ
โ โ
โ JSON / API / user input โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ treat as unknown
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ unknown โ
โ โ
โ Narrow with typeof, predicates, libraries โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ validated
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Specific type โ
โ โ
โ User, Product, Config, etc. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ work with typed value
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Application code โ
โ โ
โ Compiler-checked, autocompleted โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: unknown vs any โ the Trade-off
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ unknown โ
โ โ
โ โข Write time: more (narrow first) โ
โ โข Runtime safety: high โ
โ โข Bugs caught: many โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ any โ
โ โ
โ โข Write time: fast โ
โ โข Runtime safety: none โ
โ โข Bugs caught: none โ
โ โ
โ The time saved writing is paid in debugging โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: never as Bottom
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Assignable to: โ
โ โ
โ never โ string โ
โ
โ never โ number โ
โ
โ never โ User โ
โ
โ never โ anything โ
โ
โ โ
โ Assignable from: โ
โ โ
โ string โ never โ โ
โ number โ never โ โ
โ anything โ never โ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Error Handling
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ Implicit any โ
โ โ
โ try { } catch (e) { โ
โ e.message; // no error, may crash โ
โ } โ
โ โ
โ (when useUnknownInCatchVariables: false) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
unknown โ
โ โ
โ try { } catch (e) { โ
โ if (e instanceof Error) e.message; โ
โ else if (typeof e === 'string') e; โ
โ } โ
โ โ
โ (with useUnknownInCatchVariables: true) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Validation Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ raw: unknown โ
โ โ โ
โ โผ โ
โ isUser(raw)? โ
โ โ โ
โ โโโ true โ raw is User (narrowed) โ
โ โ โ
โ โโโ false โ throw or handle โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Decision Tree
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Do you know the type? โ
โ โ โ
โ โโโ Yes โโโบ Use the type โ
โ โ โ
โ โโโ No โ
โ โ โ
โ โโโ Will you narrow it? โ
โ โ โ โ
โ โ โโโ Yes โโโบ unknown โ
โ โ โ โ
โ โ โโโ No โโโบ never? or any? โ
โ โ โ
โ โโโ Does it throw or exhaust? โ
โ โ โ
โ โโโ Yes โโโบ never โ
โ โ โ
โ โโโ No โโโบ any (last โ
โ resort) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Type | Position | Assignable to it | Assignable from it | Safe |
|---|---|---|---|---|
unknown | Top | Everything | Nothing (without narrowing) | โ |
any | Escape | Everything | Everything | โ |
never | Bottom | Nothing | Everything | โ |
Key takeaways:
unknownis the top type โ accept anything, use nothing without narrowinganyis the escape hatch โ accept and assign everything, disables type checkingneveris the bottom type โ no values, assignable to everything, used for exhaustiveness- Use
unknownfor external data โ JSON, API, user input, catch variables - Use
neverfor exhaustiveness checks and throwing functions - Avoid
anyunless absolutely necessary โ preferunknownwith validation unknownrequires narrowing โtypeof,instanceof,in, predicates, validation librariesanypropagates โ every operation on it produces anotheranystrictenablesnoImplicitAnyanduseUnknownInCatchVariablesJSON.parsereturnsanyโ assign tounknownand validateneverin conditional types filters union members โT extends X ? never : Tneveris assignable to everything โ perfect for functions that never return- Migrate
anytounknownfirst, then narrow with predicates or validation libraries - Mark deliberate
anywith a comment or ESLint disable โ it’s an exception, not a default
Remember: unknown, any, and never are the three extremes of TypeScript’s type system. unknown is safe but demanding โ it forces you to narrow. any is convenient but dangerous โ it disables the checks that catch bugs. never is empty but powerful โ it proves exhaustiveness and marks unreachable code. Reach for unknown at every boundary, never for exhaustiveness, and any only when there’s no alternative. That discipline keeps the type system doing its job.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!