Logo

Home

About

Blog

Contact

Guestbook

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. The any, unknown, and never Types

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Basic Types in TypeScript
TypeScript
9m
Nov 23, 2025

Basic Types in TypeScript

TypeScript's type system starts with fundamental building blocks: primitive types like string, number, and boolean, along with arrays and type annotations. Understanding these basic types is essential for writing type-safe code. This comprehensive guide walks you through each primitive type, shows you how to use type annotations effectively, and teaches you best practices for working with arrays and basic data structures in TypeScript.

#TypeScript#Types+5
Typing Functions: Parameters and Returns
TypeScript
8m
Dec 18, 2025

Typing Functions: Parameters and Returns

Functions are the building blocks of any program. In TypeScript, typing function parameters and return values helps catch errors early and improves code readability. This guide covers function signatures, typing parameters and returns, void functions, and implicit return types.

#TypeScript#Functions+4
Type Assertions and Type Casting
TypeScript
6m
Dec 13, 2025

Type Assertions and Type Casting

Type assertions tell TypeScript to trust your judgment about a value's type. While they're powerful tools for working with DOM elements, API responses, and type narrowing, they can also bypass type safety if misused. This guide covers the as syntax, angle-bracket syntax, when to use assertions safely, and the crucial difference between TypeScript assertions and runtime type casting. Learn to wield this tool responsibly while maintaining type safety in your applications.

#TypeScript#Type Assertions+4

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2025 • All rights reserved

Contents

  • Understanding Special Types
  • The any Type
    • What is any?
    • When to Use any
    • The Dangers of any
    • Implicit any
  • The unknown Type
    • What is unknown?
    • Type Narrowing with unknown
    • Common Patterns with unknown
    • unknown vs any
  • The never Type
    • What is never?
    • Functions That Never Return
    • Exhaustive Type Checking
    • Impossible Intersections
  • Practical Examples
    • API Response Handling
    • Error Handling
    • Type Guards
    • State Machines
  • Type Hierarchy
  • Migration Strategies
  • Best Practices
  • Common Mistakes
  • Next Steps
TypeScript

The any, unknown, and never Types

December 8, 2025•15 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript special types visualization
The any, unknown, and never Types

The any, unknown, and never Types#

TypeScript's type system includes three special types that handle edge cases: any (the escape hatch that disables type checking), unknown (the type-safe alternative to any), and never (the type that represents values that never occur). Understanding these types is essential for writing robust TypeScript code that balances flexibility with safety.

Quick Summary: Use unknown instead of any when possible, and leverage never for exhaustive checking. These types are the foundation for advanced TypeScript patterns.

Understanding Special Types

TypeScript's type hierarchy includes these special types:

// Type hierarchy (simplified)
never     → Bottom type (nothing is assignable to it except never)
  ↓
specific  → string, number, boolean, etc.
  ↓
unknown   → Top type (all types assignable to it)
  ↓
any       → Escape hatch (opts out of type checking)

Each serves a distinct purpose in TypeScript's type system.

The any Type

What is any?

any is TypeScript's escape hatch—it opts out of type checking completely:

let value: any;
 
value = 42; // ✓ OK
value = "hello"; // ✓ OK
value = true; // ✓ OK
value = { x: 10 }; // ✓ OK
value = [1, 2, 3]; // ✓ OK
 
// No type checking on operations
value.toUpperCase(); // ✓ No error (runtime error if value isn't a string)
value.nonExistentMethod(); // ✓ No error (runtime error)
let x: number = value; // ✓ No error

When you use any, TypeScript essentially treats it like JavaScript—no type checking at all.

Key characteristics:

  • Can be assigned any value
  • Can be assigned to any type
  • All operations are allowed
  • No autocomplete or type hints

When to Use any

Despite its dangers, any has legitimate use cases:

1. Migrating JavaScript to TypeScript:

// Temporarily use any during migration
let legacyData: any = getLegacyData();
 
// Plan to replace with proper types later
interface LegacyData {
  // ... proper types
}

2. Interacting with untyped third-party libraries:

// Third-party library without type definitions
declare const thirdPartyLib: any;
 
let result = thirdPartyLib.doSomething();

3. Truly dynamic data structures:

// JSON parsing (though unknown is better)
let data: any = JSON.parse(jsonString);

4. Prototyping or debugging:

// Quick prototype - fix types later
function quickTest(data: any) {
  console.log(data);
}

Important: These should be temporary solutions. Always aim to replace any with proper types or at least unknown.

The Dangers of any

Using any defeats the purpose of TypeScript:

function processUser(user: any) {
  // TypeScript allows this even though it might crash
  console.log(user.name.toUpperCase());
  return user.age * 2;
}
 
// These all compile without errors
processUser({ name: "Alice", age: 30 }); // Works
processUser({ name: null, age: 30 }); // Runtime error!
processUser({}); // Runtime error!
processUser(null); // Runtime error!

Problems with any:

  • Loses all type safety
  • No IDE autocomplete
  • Runtime errors aren't caught
  • Makes refactoring dangerous
  • Spreads through your codebase (type pollution)

Type pollution example:

function getData(): any {
  return { name: "Alice" };
}
 
let data = getData(); // Type: any
let name = data.name; // Type: any (pollution spreads!)
let upper = name.toUpperCase(); // Type: any (still spreading!)

Implicit any

TypeScript can infer any in some cases:

// Implicit any (with noImplicitAny: false)
function log(message) {
  // Parameter 'message' implicitly has 'any' type
  console.log(message);
}
 
let x; // Variable 'x' implicitly has type 'any'
x = 5;
x = "hello";

Best Practice: Always enable noImplicitAny in your tsconfig.json to catch these cases:

{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

The unknown Type

What is unknown?

unknown is the type-safe counterpart to any—you can assign anything to it, but you must check the type before using it:

let value: unknown;
 
value = 42; // ✓ OK
value = "hello"; // ✓ OK
value = true; // ✓ OK
value = { x: 10 }; // ✓ OK
 
// But you can't use it without checking
value.toUpperCase(); // ✗ Error: Object is of type 'unknown'
let x: number = value; // ✗ Error: Type 'unknown' is not assignable to type 'number'
 
// Must narrow the type first
if (typeof value === "string") {
  value.toUpperCase(); // ✓ OK - TypeScript knows it's a string here
}

Key characteristics:

  • Can be assigned any value (like any)
  • Cannot be assigned to other types without checking
  • Cannot perform operations without type narrowing
  • Forces you to handle types safely

Type Narrowing with unknown

You must narrow unknown to a specific type before using it:

1. Using typeof:

function processValue(value: unknown) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // value is string here
  } else if (typeof value === "number") {
    console.log(value.toFixed(2)); // value is number here
  } else if (typeof value === "boolean") {
    console.log(value ? "yes" : "no"); // value is boolean here
  }
}

2. Using instanceof:

function handleError(error: unknown) {
  if (error instanceof Error) {
    console.log(error.message); // error is Error here
  } else {
    console.log("Unknown error:", error);
  }
}

3. Using type guards:

function isString(value: unknown): value is string {
  return typeof value === "string";
}
 
function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}
 
function process(value: unknown) {
  if (isString(value)) {
    console.log(value.toUpperCase()); // value is string
  } else if (isObject(value)) {
    console.log(Object.keys(value)); // value is object
  }
}

4. Using in operator:

function processData(data: unknown) {
  if (
    typeof data === "object" &&
    data !== null &&
    "name" in data &&
    "age" in data
  ) {
    // data is object with name and age properties
    let obj = data as { name: unknown; age: unknown };
    console.log(obj.name, obj.age);
  }
}

Common Patterns with unknown

JSON parsing:

function parseJSON(jsonString: string): unknown {
  return JSON.parse(jsonString);
}
 
let data = parseJSON('{"name": "Alice", "age": 30}');
 
// Must validate before use
if (
  typeof data === "object" &&
  data !== null &&
  "name" in data &&
  "age" in data
) {
  let user = data as { name: string; age: number };
  console.log(user.name); // Safe to use
}

API responses:

async function fetchData(url: string): Promise<unknown> {
  let response = await fetch(url);
  return response.json();
}
 
let data = await fetchData("/api/users");
 
// Validate structure
function isUser(value: unknown): value is { name: string; age: number } {
  return (
    typeof value === "object" &&
    value !== null &&
    "name" in value &&
    typeof (value as any).name === "string" &&
    "age" in value &&
    typeof (value as any).age === "number"
  );
}
 
if (isUser(data)) {
  console.log(data.name); // Type-safe
}

Error handling:

try {
  riskyOperation();
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error(error.message);
  } else if (typeof error === "string") {
    console.error(error);
  } else {
    console.error("An unknown error occurred");
  }
}

unknown vs any

Comparing the two:

// any - unsafe
let anyValue: any = "hello";
anyValue.toFixed(2); // No error, but runtime crash!
 
// unknown - safe
let unknownValue: unknown = "hello";
unknownValue.toFixed(2); // ✗ Error: Object is of type 'unknown'
 
if (typeof unknownValue === "number") {
  unknownValue.toFixed(2); // ✓ OK - type checked first
}

When to use each:

Use CaseUse anyUse unknown
Migrating JS code✓ Temporary✓ Better
JSON parsing✗✓
Error handling✗✓
Truly dynamic data✗✓
Quick prototyping✓✗
Third-party untyped libs✓✓

Rule of Thumb: Always prefer unknown over any. Only use any when you have no other choice and plan to fix it later.

The never Type

What is never?

never represents values that never occur—it's the bottom type in TypeScript's type system:

// Function that never returns normally
function throwError(message: string): never {
  throw new Error(message);
}
 
// Infinite loop - never returns
function infiniteLoop(): never {
  while (true) {
    // Loop forever
  }
}
 
// Variable that can never have a value
let impossible: never;
impossible = 5; // ✗ Error: Type 'number' is not assignable to type 'never'
impossible = "hi"; // ✗ Error: Type 'string' is not assignable to type 'never'

Key characteristics:

  • Represents values that never occur
  • No value can be assigned to never (except never itself)
  • never can be assigned to any type
  • Used for unreachable code and exhaustive checking

Functions That Never Return

Functions return never when they can't complete normally:

// Throws an error
function fail(message: string): never {
  throw new Error(message);
}
 
// Infinite loop
function keepProcessing(): never {
  while (true) {
    processNextTask();
  }
}
 
// Process exits
function exit(code: number): never {
  process.exit(code);
}
 
// Using never in control flow
function processValue(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase();
  } else if (typeof value === "number") {
    return value.toFixed(2);
  }
 
  // TypeScript knows this is unreachable
  fail("Invalid value type");
  // No return needed - never indicates function doesn't return
}

Exhaustive Type Checking

never enables exhaustive checking in switch statements and conditionals:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; width: number; height: number };
 
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.width * shape.height;
    default:
      // If we forget a case, this catches it
      let exhaustiveCheck: never = shape;
      throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
  }
}

Adding a new type:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number }; // New!
 
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.width * shape.height;
    default:
      // ✗ Error: Type 'triangle' is not assignable to type 'never'
      let exhaustiveCheck: never = shape;
      throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
  }
}
// TypeScript forces us to handle the triangle case!

Best Practice: Always use exhaustive checking in switch statements for union types. It catches bugs when you add new cases.

Helper function for exhaustive checks:

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}
 
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.width * shape.height;
    default:
      return assertNever(shape); // Compile error if not exhaustive
  }
}

Impossible Intersections

never appears when type intersections are impossible:

type StringAndNumber = string & number; // Type: never
// Can't be both string AND number
 
type A = { type: "a"; value: string };
type B = { type: "b"; value: number };
 
type Impossible = A & B; // Type: never
// Can't have type "a" and type "b" simultaneously
 
// Useful in conditional types
type OnlyStrings<T> = T extends string ? T : never;
 
type Result = OnlyStrings<string | number | boolean>;
// Type: string (number and boolean become never)

Filtering with never:

type NonNullable<T> = T extends null | undefined ? never : T;
 
type Result = NonNullable<string | number | null | undefined>;
// Type: string | number

Practical Examples

API Response Handling

interface ApiResponse<T> {
  status: "success" | "error";
  data?: T;
  error?: string;
}
 
async function fetchUser(id: number): Promise<ApiResponse<unknown>> {
  try {
    let response = await fetch(`/api/users/${id}`);
    let data = await response.json();
    return { status: "success", data };
  } catch (error: unknown) {
    if (error instanceof Error) {
      return { status: "error", error: error.message };
    }
    return { status: "error", error: "Unknown error" };
  }
}
 
// Using the response
let response = await fetchUser(1);
 
if (response.status === "success" && response.data) {
  let data = response.data;
 
  // Validate structure
  if (
    typeof data === "object" &&
    data !== null &&
    "name" in data &&
    "email" in data
  ) {
    let user = data as { name: string; email: string };
    console.log(`User: ${user.name} (${user.email})`);
  }
} else {
  console.error("Error:", response.error);
}

Error Handling

class NetworkError extends Error {
  constructor(
    message: string,
    public statusCode: number
  ) {
    super(message);
    this.name = "NetworkError";
  }
}
 
class ValidationError extends Error {
  constructor(
    message: string,
    public field: string
  ) {
    super(message);
    this.name = "ValidationError";
  }
}
 
function handleError(error: unknown): never {
  if (error instanceof NetworkError) {
    console.error(`Network error (${error.statusCode}):`, error.message);
  } else if (error instanceof ValidationError) {
    console.error(`Validation error in ${error.field}:`, error.message);
  } else if (error instanceof Error) {
    console.error("Error:", error.message);
  } else {
    console.error("Unknown error:", error);
  }
 
  process.exit(1); // Function never returns
}
 
try {
  riskyOperation();
} catch (error: unknown) {
  handleError(error); // Returns never, so no code after this runs
}

Type Guards

interface User {
  type: "user";
  name: string;
  email: string;
}
 
interface Admin {
  type: "admin";
  name: string;
  email: string;
  permissions: string[];
}
 
type Account = User | Admin;
 
function isAdmin(account: unknown): account is Admin {
  return (
    typeof account === "object" &&
    account !== null &&
    "type" in account &&
    account.type === "admin" &&
    "permissions" in account &&
    Array.isArray((account as Admin).permissions)
  );
}
 
function processAccount(account: unknown) {
  if (isAdmin(account)) {
    // TypeScript knows account is Admin here
    console.log(`Admin: ${account.name}`);
    console.log(`Permissions: ${account.permissions.join(", ")}`);
  } else {
    console.log("Not an admin account");
  }
}

State Machines

type State =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string }
  | { status: "error"; error: string };
 
function renderState(state: State): string {
  switch (state.status) {
    case "idle":
      return "Ready to start";
    case "loading":
      return "Loading...";
    case "success":
      return `Data: ${state.data}`;
    case "error":
      return `Error: ${state.error}`;
    default:
      // Exhaustive check
      let exhaustive: never = state;
      throw new Error(`Unhandled state: ${exhaustive}`);
  }
}
 
// Using the state machine
let currentState: State = { status: "idle" };
console.log(renderState(currentState)); // "Ready to start"
 
currentState = { status: "loading" };
console.log(renderState(currentState)); // "Loading..."
 
currentState = { status: "success", data: "Hello, World!" };
console.log(renderState(currentState)); // "Data: Hello, World!"

Type Hierarchy

Understanding the relationships:

// never is assignable to everything
let n: never = getNever();
let str: string = n; // ✓ OK
let num: number = n; // ✓ OK
let obj: object = n; // ✓ OK
 
// Nothing (except never) is assignable to never
let n2: never;
n2 = 5; // ✗ Error
n2 = "hello"; // ✗ Error
n2 = getNever(); // ✓ OK
 
// Everything is assignable to unknown
let u: unknown;
u = 5; // ✓ OK
u = "hello"; // ✓ OK
u = { x: 10 }; // ✓ OK
 
// unknown is not assignable to other types (without checking)
let u2: unknown = "hello";
let str2: string = u2; // ✗ Error
 
// any breaks the type system
let a: any = 5;
let str3: string = a; // ✓ OK (but unsafe!)
a = "hello";
let num2: number = a; // ✓ OK (but unsafe!)

Subtype relationships:

never
  ↓ (subtype of)
literal types (e.g., "hello", 42)
  ↓
primitive types (string, number, etc.)
  ↓
object types
  ↓
unknown

any (breaks the hierarchy)

Migration Strategies

Migrating from any to unknown:

Find all any usages:

# Search your codebase
grep -r ": any" src/

Replace with unknown:

// Before
function process(data: any) {
  return data.value;
}
 
// After
function process(data: unknown) {
if (typeof data === "object" && data !== null && "value" in data) {
return (data as { value: unknown }).value;
}
throw new Error("Invalid data");
}
 

Add type guards:

function isValidData(data: unknown): data is { value: string } {
  return (
    typeof data === "object" &&
    data !== null &&
    "value" in data &&
    typeof (data as any).value === "string"
  );
}
 
function process(data: unknown) {
  if (isValidData(data)) {
    return data.value; // Type-safe!
  }
  throw new Error("Invalid data");
}

Test thoroughly:

// Add tests for edge cases
process({ value: "hello" });  // Works
process({});                  // Throws
process(null);                // Throws
process("string");            // Throws

Best Practices

1. Prefer unknown over any:

// ✗ Avoid
function parse(json: string): any {
  return JSON.parse(json);
}
 
// ✓ Better
function parse(json: string): unknown {
  return JSON.parse(json);
}

2. Use type guards with unknown:

// ✓ Good
function process(value: unknown) {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  throw new Error("Expected string");
}

3. Use never for exhaustive checking:

// ✓ Good - catches missing cases at compile time
function handle(value: "a" | "b") {
  switch (value) {
    case "a":
      return "A";
    case "b":
      return "B";
    default:
      let exhaustive: never = value;
      throw new Error(`Unhandled: ${exhaustive}`);
  }
}

4. Enable strict compiler options:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true
  }
}

5. Document why you use any:

// If you must use any, explain why
let data: any; // TODO: Add proper types after API stabilizes

Common Mistakes

Mistake 1: Using any when unknown would work:

// ✗ Bad
function log(data: any) {
  console.log(data);
}
 
// ✓ Better
function log(data: unknown) {
  console.log(data); // console.log accepts unknown
}

Mistake 2: Not narrowing unknown before use:

// ✗ Bad
function process(data: unknown) {
  return data.value; // Error!
}
 
// ✓ Good
function process(data: unknown) {
  if (typeof data === "object" && data !== null && "value" in data) {
    return (data as { value: unknown }).value;
  }
}

Mistake 3: Forgetting exhaustive checks:

type Status = "pending" | "active" | "done";
 
// ✗ Risky - no compile error if you add a new status
function getMessage(status: Status): string {
  if (status === "pending") return "Pending...";
  if (status === "active") return "Active!";
  if (status === "done") return "Done!";
  return "Unknown"; // Silently handles new cases
}
 
// ✓ Better - compile error if you add a new status
function getMessage(status: Status): string {
  switch (status) {
    case "pending":
      return "Pending...";
    case "active":
      return "Active!";
    case "done":
      return "Done!";
    default:
      let exhaustive: never = status;
      throw new Error(`Unhandled: ${exhaustive}`);
  }
}

Mistake 4: Assigning never inappropriately:

// ✗ Bad - never should be for truly impossible values
let value: never = 42; // Error: Type 'number' is not assignable to type 'never'
 
// ✓ Good - never for functions that don't return
function throwError(): never {
  throw new Error();
}

Next Steps

You now understand TypeScript's special types and how to use them effectively! Here's what to explore next:

Practice These Concepts:

  • Replace any with unknown in your existing code
  • Add exhaustive checking to your switch statements
  • Create type guards for common data structures
  • Use never to catch missing cases at compile time

Continue Learning:

  • Type guards and type predicates
  • Advanced union and intersection types
  • Conditional types
  • Mapped types and utility types

Congratulations! You've mastered any, unknown, and never. These types are fundamental to writing safe, flexible TypeScript code. Use unknown by default, leverage never for exhaustive checking, and reserve any only for true edge cases.


Questions about these special types? Struggling with type narrowing? Share your use case in the comments and let's work through it together!