| |

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.

TypeAssignable to itAssignable from itSafe?
unknownEverythingNothing without narrowingโœ…
anyEverythingEverythingโŒ
neverNothingEverythingโœ…

Reading the table:

  • unknown accepts any value, but you can’t use it until you narrow
  • any accepts any value and can be used as anything โ€” no checks
  • never accepts 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. unknown is the top โ€” the widest possible type. never is the bottom โ€” the narrowest. any is 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
  • unknown is assignable only to unknown or any
  • 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) under useUnknownInCatchVariables

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 unknown is the “safe any”: It has the same acceptance range โ€” anything can be assigned โ€” but it can’t be used without narrowing. Where any silently lets you write code that crashes at runtime, unknown forces 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 any becomes any
  • 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 unknown will 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:

NeedBetter option
Unknown external dataunknown
“Anything works here”unknown + validation
Generic flexibilityType parameter <T>
JSON outputunknown then validate
Arbitrary objectRecord<string, unknown>
Library without typesunknown + custom .d.ts
Silencing errorsFix the type

Why any is not banned: TypeScript’s design team kept any because some scenarios genuinely need it โ€” during migrations, when bridging untyped code. But the guidance is clear: prefer unknown in almost every case. any is 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 โ€” throw or 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.

Propertyunknownanynever
Top of hierarchyโœ…โŒโŒ
Bottom of hierarchyโŒโŒโœ…
Assignable to itEverythingEverythingNothing
Assignable from itNothing without narrowingEverythingEverything
Usable without narrowingโŒโœ…N/A (no values)
Type checkingFull (after narrowing)DisabledFull
Safe?โœ…โŒโœ…
Common useExternal dataEscape hatchExhaustiveness

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:

ExpressionResult
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:

SignatureAccepts
(x: unknown) => voidAny value
(x: any) => voidAny value
(x: never) => voidNothing โ€” 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. unknown forces you to check; any disables checking; never can’t hold anything. Knowing which to reach for is the difference between safe code and silent bugs. The rule: unknown by default, never for exhaustiveness, any never.


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:

ScenarioType
JSON.parse resultunknown
fetch().json() resultunknown
Catch variableunknown
Event handler with unknown shapeunknown
Function returns from thrownever
Exhaustive switch defaultnever
Legacy libraryTry unknown first

Migration path: When you find any in your code, ask:

  1. Can this be unknown with validation? โ†’ Replace
  2. Can this be a proper type? โ†’ Replace
  3. Can this be generic? โ†’ Replace
  4. Is any genuinely required? โ†’ Keep, with a comment

Most any uses become unknown with a guard or a proper type.

Why the preference is clear: unknown provides the same flexibility as any โ€” accept anything โ€” but requires explicit handling. That requirement is exactly what you want at boundaries. any lets you write code that type-checks but crashes. unknown forces the check that prevents the crash. There’s almost never a reason to pick any over unknown in 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: unknown can 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 : any and as any
  • Use unknown in every location where any appeared

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 any spreads: TypeScript’s type checker treats any as a wildcard. Any operation on an any produces another any. That cascading effect is why a single any can hide bugs across a whole module. unknown doesn’t spread โ€” every operation on unknown requires 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:

  • unknown for the JSON response
  • A predicate narrows to User
  • unknown for the catch variable
  • never for the throwing helper
  • assertNever for exhaustiveness

Why this shape: It’s how real boundary code should look. External data is unknown until validated. Errors are unknown until narrowed. Nothing is any. 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

Propertyunknownanynever
PositionTopEscapeBottom
Assignable to itAnythingAnythingNothing
Assignable from itNothing (without narrowing)AnythingAnything
Safeโœ…โŒโœ…
PurposeExternal dataEscape hatchExhaustiveness

Assignability

From โ†’ Tounknownanynever
unknown โ†’ stringโŒโ€”โ€”
any โ†’ stringโ€”โœ…โ€”
never โ†’ stringโ€”โ€”โœ…
string โ†’ unknownโœ…โ€”โ€”
string โ†’ anyโ€”โœ…โ€”
string โ†’ neverโ€”โ€”โŒ

When to Use Each

Use caseType
External JSONunknown
Catch variableunknown
API responseunknown
User inputunknown
Untyped libraryunknown first
Function that throwsnever
Exhaustive switch defaultnever
Impossible intersectionnever
Legacy migrationany (temporarily)
Absolutely no alternativeany (with comment)

Narrowing from unknown

ToolExample
typeoftypeof x === 'string'
instanceofx instanceof Error
in'name' in x
Array.isArrayArray.isArray(x)
PredicateisUser(x): x is User
LibrarySchema.safeParse(x)

unknown in Function Signatures

SignatureAccepts
(x: unknown) => voidAnything
(x: any) => voidAnything
(x: never) => voidNothing

any Propagation

OperationResult type
const x: anyany
x.fooany
x.foo.barany
x()any
x + 1any
[x]any[]
{ a: x }{ a: any }

never Appearances

ContextExample
Function returnfunction f(): never { throw }
ExhaustivenessAfter all cases
Impossible typestring & number
Conditional filterT extends X ? never : T
Union identityA | never = A
Intersection absorbingA & never = never
Unreachable codeAfter return/throw

Common Patterns

PatternType
Boundary validationunknown + predicate
Exhaustivenessnever + assertNever
Throwing helpernever return
Error narrowingunknown in catch
Filter nullishT extends null ? never : T

Anti-Patterns

PatternProblem
: any as defaultDisables checks
as any to silence errorsHides bugs
JSON.parse() as FooNo validation
catch (e: any)Unchecked errors
Returning any from functionsSpreads to callers
never as function parameterUncallable

strict Flags Related

FlagEffect
noImplicitAnyReject implicit any
useUnknownInCatchVariablesCatch is unknown
strictNullChecksRelated null safety

Migration Cheatsheet

BeforeAfter
anyunknown + predicate
anyType parameter
anyProper interface
catch (e: any)catch (e) โ€” unknown
JSON.parse(x) as Tunknown + 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

PitfallProblemSolution
any as default typeDisables checksUse unknown
as any to silenceHides bugsFix the type
No strictMany implicit anysEnable strict
JSON.parse() as TNo validationValidate with predicate
Returning anySpreads to callersReturn specific type
never as parameterUncallableUse only for exhaustiveness helpers
Confusing unknown and anyDifferent safetyunknown requires narrowing
Not narrowing unknownCompile errorUse typeof/instanceof
Empty catch blockSwallows errorsHandle the error
any in genericsDefeats genericUse 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

TypePositionAssignable to itAssignable from itSafe
unknownTopEverythingNothing (without narrowing)โœ…
anyEscapeEverythingEverythingโŒ
neverBottomNothingEverythingโœ…

Key takeaways:

  • unknown is the top type โ€” accept anything, use nothing without narrowing
  • any is the escape hatch โ€” accept and assign everything, disables type checking
  • never is the bottom type โ€” no values, assignable to everything, used for exhaustiveness
  • Use unknown for external data โ€” JSON, API, user input, catch variables
  • Use never for exhaustiveness checks and throwing functions
  • Avoid any unless absolutely necessary โ€” prefer unknown with validation
  • unknown requires narrowing โ€” typeof, instanceof, in, predicates, validation libraries
  • any propagates โ€” every operation on it produces another any
  • strict enables noImplicitAny and useUnknownInCatchVariables
  • JSON.parse returns any โ€” assign to unknown and validate
  • never in conditional types filters union members โ€” T extends X ? never : T
  • never is assignable to everything โ€” perfect for functions that never return
  • Migrate any to unknown first, then narrow with predicates or validation libraries
  • Mark deliberate any with 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!