Skip to main content

TS Ep 6: Any vs. Unknown — Top Types and Type Safety

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 6: This Article
In TypeScript’s type hierarchy, any and unknown sit at the very top as universal types that accept any value. However, they operate under completely opposite guarantees: any turns off the compiler, while unknown enforces absolute type verification before usage.

1. Top Types in Set Theory
#

In type theory, a Top Type (denoted as $\top$) is a universal supertype. Every possible value in JavaScript—numbers, strings, objects, functions, symbols, null, undefined—is a subtype of a top type.

TypeScript has two top types:

  1. any: An unsound top type. It represents any value AND can be assigned to any type without checks.
  2. unknown: A sound top type. It represents any value, BUT cannot be assigned to other types or operated upon without type narrowing.

flowchart TD
    TopAny["any (Disables Checker)"] --> Primitives["string | number | boolean | object"]
    TopUnknown["unknown (Enforces Verification)"] --> Primitives
    Primitives --> BottomNever["never (Empty Set)"]

2. The Danger of any (Type Contagion)
#

When a variable is typed as any, TypeScript completely turns off the static type checker for that identifier.

let dynamicData: any = "Hello World";

// ❌ TypeScript WILL NOT flag any of these catastrophic errors at compile time:
dynamicData.nonExistentMethod();  // Runtime TypeError!
dynamicData();                    // Runtime TypeError!
dynamicData.a.b.c.d;             // Runtime TypeError!

The Problem of “Any Contagion”
#

The most dangerous property of any is its ability to contaminate surrounding code. When an any value is passed into a function or assigned to another variable, it implicitly infects those targets with any:

function getRawData(): any {
  return { id: 101, title: "Post" };
}

// 'result' is silently inferred as 'any'!
const result = getRawData();

// 'title' is also silently 'any'!
const title = result.title; 

// Now 'uppercaseTitle' is 'any', and errors propagate everywhere unnoticed!
const uppercaseTitle = title.toUppercase(); // Typo! (.toUpperCase())

Because of any contagion, a single any in a data-access layer can silently disable type safety across your entire application codebase.


3. The Safety of unknown
#

Introduced in TypeScript 3.0, unknown is the type-safe alternative to any. Like any, any value can be assigned to an unknown variable. However, no operations can be performed on an unknown value until you narrow its type.

let apiResponse: unknown = "Response payload";

// ❌ Compiler Error: Property 'toUpperCase' does not exist on type 'unknown'.
// apiResponse.toUpperCase();

// ❌ Compiler Error: Type 'unknown' is not assignable to type 'string'.
// let message: string = apiResponse; 

Unlocking unknown via Type Narrowing
#

To perform operations on an unknown value, you must first prove its concrete type using runtime checks (typeof, instanceof, or custom type guards):

let apiResponse: unknown = "Response payload";

// 🟢 WORKED: Type narrowed using typeof
if (typeof apiResponse === "string") {
  // Inside this block, TypeScript narrows apiResponse to 'string'
  console.log(apiResponse.toUpperCase()); // Valid!
}

4. any vs unknown Comparison Matrix
#

Propertyanyunknown
Assign anything to it?YES (let x: any = 5)YES (let x: unknown = 5)
Assign it to other typed variables?YES (Unsafe!)NO (Compiler Error!)
Access properties directly?YES (x.foo.bar)NO (Compiler Error!)
Call as a function directly?YES (x())NO (Compiler Error!)
Requires narrowing before use?NOYES
Type Safety GuaranteeNone (0%)Maximum (100%)

5. Modern Best Practices for unknown
#

1. Handling JSON.parse()
#

JSON.parse() natively returns any in standard TypeScript lib definitions. Cast the result immediately to unknown:

function parseJSON(rawJson: string): unknown {
  return JSON.parse(rawJson);
}

const data = parseJSON('{"name": "Alice", "age": 30}');

// Narrow before accessing properties
if (
  typeof data === "object" && 
  data !== null && 
  "name" in data && 
  typeof (data as Record<string, unknown>).name === "string"
) {
  console.log((data as { name: string }).name.toUpperCase());
}

2. Error Catch Blocks (useUnknownInCatchVariables)
#

In JavaScript, try { ... } catch (error) can catch anything—not just Error instances (e.g., a library might throw "Fatal error" or throw 404).

In modern TypeScript ("useUnknownInCatchVariables": true in tsconfig.json), error variables in catch blocks default to unknown instead of any:

try {
  // Potentially throwing operation
  JSON.parse("invalid json");
} catch (error: unknown) {
  // ❌ Error: Property 'message' does not exist on type 'unknown'.
  // console.log(error.message);

  // 🟢 GOOD: Safely narrow before accessing .message
  if (error instanceof Error) {
    console.log(`Error message: ${error.message}`);
  } else {
    console.log(`Unknown thrown exception: ${String(error)}`);
  }
}

Summary & Next Steps
#

In this episode:

  • We analyzed top types in set theory ($\top$).
  • We demonstrated how any causes type contagion, disabling checks downstream.
  • We proved why unknown is the sound, type-safe alternative requiring runtime narrowing.
  • We configured catch blocks with error: unknown and safe JSON.parse wrappers.

In Episode 7: Union and Intersection Types, we will explore how to combine types using | (OR) and & (AND)!

typescript - This article is part of a series.
Part 6: This Article