unknown → Safe Types: Runtime Validation Patterns

Stop lying with `as`. Learn production patterns to convert unknown data (fetch, localStorage, env) into safe TypeScript types with guards and assertions.

F

Frontend Interview Team

March 01, 2026

2 min read
unknown → Safe Types: Runtime Validation Patterns

What you’ll learn

  • Why unknown is the correct type for external data
  • Type guards vs assertion functions (asserts)
  • A production pattern for validating API responses and storage

30‑second interview answer

When data comes from outside TypeScript (network, storage, user input), it should start as unknown. Converting unknown to a safe type requires runtime validation. Type guards (x is T) and assertion functions (asserts x is T) let you validate once at the boundary and keep the rest of the codebase type-safe without as casts.


The core truth: TypeScript can’t validate at runtime

This compiles and can still crash:

type User = { id: string; name: string };
 
const user = (await fetch("/api/user").then(r => r.json())) as User;
// If API returns { id: 123 }, your code is broken but TS is silent.

The fix is: treat external data as unknown.


Step 1: Start with unknown

const data: unknown = await fetch("/api/user").then(r => r.json());

Step 2: Validate with a type guard

type User = { id: string; name: string };
 
function isRecord(x: unknown): x is Record<string, unknown> {
  return typeof x === "object" && x !== null;
}
 
function isUser(x: unknown): x is User {
  if (!isRecord(x)) return false;
  return typeof x.id === "string" && typeof x.name === "string";
}
 
async function getUser(): Promise<User> {
  const data: unknown = await fetch("/api/user").then(r => r.json());
  if (!isUser(data)) throw new Error("Invalid API response: User");
  return data;
}

Now getUser() is safe.


Assertion functions (asserts) for better ergonomics

Sometimes you want validation but keep the value name.

function assertUser(x: unknown): asserts x is User {
  if (!isUser(x)) throw new Error("Invalid User");
}
 
const data: unknown = JSON.parse(localStorage.getItem("user") ?? "null");
assertUser(data);
// data is now User

Pattern: validate env vars once

type Env = {
  SITE_URL: string;
  MONGODB_URI: string;
};
 
function assertEnv(x: any): asserts x is Env {
  if (typeof x.SITE_URL !== "string") throw new Error("Missing SITE_URL");
  if (typeof x.MONGODB_URI !== "string") throw new Error("Missing MONGODB_URI");
}
 
const env = process.env;
assertEnv(env);
// env is Env

This is clean, and it keeps unsafe process.env usage out of your app.


Common mistakes

  • Using as T to silence errors instead of validating
  • Writing guards that only check a single field (false confidence)
  • Skipping boundary validation, then chasing bugs deep in the app

Production rule of thumb

  • External data starts as unknown.
  • Validate at the boundary.
  • Inside the app, keep types strong and casts rare.

Interview questions

  1. Q: Why is unknown better than any?

    • A: unknown forces validation before use; any disables safety.
  2. Q: What’s the difference between a type guard and an assertion function?

    • A: Guards return boolean (x is T); assertions throw if invalid and narrow the type (asserts x is T).

Quick recap

  • unknown is the correct starting point for untrusted data.
  • Use guards/assertions to validate.
  • This pattern scales across fetch, storage, and env.