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.
Frontend Interview Team
March 01, 2026
What you’ll learn
- Why
unknownis 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 UserPattern: 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 EnvThis is clean, and it keeps unsafe process.env usage out of your app.
Common mistakes
- Using
as Tto 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
-
Q: Why is
unknownbetter thanany?- A:
unknownforces validation before use;anydisables safety.
- A:
-
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).
- A: Guards return boolean (
Quick recap
unknownis the correct starting point for untrusted data.- Use guards/assertions to validate.
- This pattern scales across fetch, storage, and env.