This is one of those interview questions that catches people out — they look similar but do opposite things. In simple language: as tells the compiler what the type is (and the compiler trusts us), while satisfies checks that the value matches a type (but keeps the original narrow type).
The mental model is: as is a one-way door that drops type info, satisfies is a checkpoint that lets the original type pass through.
as — the type assertion (a.k.a. cast)
as says “trust me, this value is of type X”. TS believes us without verifying — that’s the danger. If we lie, we’ll get a runtime error with no compile-time warning.
const input = document.querySelector("input") as HTMLInputElement;
input.value = "hello"; // works — but if the selector found nothing, this crashes at runtime
The valid uses are narrow: when we have more info than the compiler can know (DOM queries, JSON parsing), and when narrowing between two types that overlap. We should treat every as like a small TODO — it’s a place where type safety stops.
const data = JSON.parse(rawJson) as { id: number; name: string };
// We're claiming JSON.parse returned this shape. If it didn't, TS won't catch it.
A common smell is using as to make TS shut up. Don’t. Fix the real type instead.
satisfies — validate without losing precision
satisfies (TS 4.9+) checks that a value matches a type, but the inferred type stays as-is — it doesn’t get widened. This solves a real problem we hit constantly.
type RouteMap = Record<string, { method: string; handler: string }>;
const routes = {
home: { method: "GET", handler: "homeHandler" },
login: { method: "POST", handler: "loginHandler" },
} satisfies RouteMap;
routes.home.method; // type is "GET", not just string
routes.unknown; // error — key doesn't exist
If we had annotated const routes: RouteMap = ... instead, routes.home.method would widen to string and routes.unknown would silently be allowed (just undefined). satisfies validates against RouteMap but keeps the literal types and the exact keys.
The classic example
Here’s the canonical case where satisfies wins. We have a config of colors, some are RGB tuples, others are hex strings, and we want both: type-checking AND the precise inferred type for each entry.
type Palette = Record<string, [number, number, number] | string>;
// With annotation — we lose the per-key type
const paletteA: Palette = {
red: [255, 0, 0],
green: "#00FF00",
};
paletteA.red.toUpperCase(); // works at compile time but crashes at runtime!
// because TS widened red to "tuple | string"
// With satisfies — TS knows each key's exact type
const paletteB = {
red: [255, 0, 0],
green: "#00FF00",
} satisfies Palette;
paletteB.red.map((n) => n * 2); // works — red is tuple
paletteB.green.toUpperCase(); // works — green is string
// paletteB.red.toUpperCase(); // error — tuple has no toUpperCase
satisfies gives us validation plus the most specific type TS can infer. Two birds, one stone.
When to use which
satisfies— almost always the right choice when defining a literal object/config that should match a contract. Default to this.as— only when bridging fromunknown-shaped data (JSON, DOM, third-party APIs) where TS truly has no way to know. Treat everyasas a risk.: Typeannotation — when we want the value’s type to be exactly the annotated type (e.g., function parameter types, public API surfaces where we want to widen on purpose).
The as const cousin
Worth mentioning since interviewers love this — as const is a special assertion that turns a literal into its narrowest possible type (readonly, literal-typed).
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"
as const + satisfies together is the modern way to define typed constants. We get the narrowest possible types AND validation.
const config = {
env: "production",
port: 3000,
} as const satisfies { env: string; port: number };
// config.env is "production", not string
Rule of thumb: reach for satisfies first, fall back to as only when truly necessary.