There are three ways to connect a value to a type in TypeScript, and they do very different things:
const a: Theme = { ... }; // annotation: "a IS a Theme" const b = { ... } as Theme; // assertion: "trust me, it's a Theme" const c = { ... } satisfies Theme; // check: "make sure it fits Theme, but keep what you know"
satisfies (TypeScript 4.9+) is the one people use least and should use more.
The problem satisfies solves
Say you have a palette where each colour is either a hex string or an RGB tuple:
type Color = string | [number, number, number]; type Palette = Record<"primary" | "danger" | "muted", Color>;
With an annotation, TypeScript checks the object, then forgets the specifics:
const palette: Palette = { primary: "#2563eb", danger: [220, 38, 38], muted: "#64748b", }; palette.primary.toUpperCase(); // ❌ Property 'toUpperCase' does not exist on type 'Color'.
It knows primary is a Color, but not which kind. You wrote a string, and the type threw that away.
With satisfies, you get the check and the precise types:
const palette = { primary: "#2563eb", danger: [220, 38, 38], muted: "#64748b", } satisfies Palette; palette.primary.toUpperCase(); // ✅ string palette.danger[0]; // ✅ number
And mistakes are still caught:
const palette = { primary: "#2563eb", dangr: [220, 38, 38], // ❌ 'dangr' does not exist in type 'Palette' } satisfies Palette; // ❌ also: property 'danger' is missing
Why satisfies is safer than as
as doesn't check that the value is correct. It tells the compiler to stop checking.
type User = { id: string; email: string; name: string }; const user = {} as User; // ✅ no error at all, and every field is missing user.email.toLowerCase(); // 💥 crashes at runtime
TypeScript only rejects an as when the two types don't overlap at all. An empty object "might" be a User, so the assertion compiles. Swap as for satisfies and the compiler lists every missing field.
Rule: use as only when you know something the compiler can't, such as the result of JSON.parse after you've validated it, or a DOM element type. Use satisfies when you want the compiler to check you.
Patterns where it shines
Config objects with literal keys
type Route = { path: string; auth: boolean }; export const routes = { home: { path: "/", auth: false }, dashboard: { path: "/dashboard", auth: true }, settings: { path: "/settings", auth: true }, } satisfies Record<string, Route>; type RouteName = keyof typeof routes; // "home" | "dashboard" | "settings"
With : Record<string, Route> instead, keyof typeof routes would just be string, and routes.typo would compile.
as const satisfies
Combine them for readonly literal values that are still checked:
export const PLANS = { free: { credits: 3, priceUsd: 0 }, pro: { credits: 100, priceUsd: 9 }, } as const satisfies Record<string, { credits: number; priceUsd: number }>; type PlanId = keyof typeof PLANS; // "free" | "pro" type ProCredits = (typeof PLANS)["pro"]["credits"]; // 100
The order matters: as const satisfies T.
Exhaustive maps over a union
type Status = "idle" | "loading" | "success" | "error"; const statusLabel = { idle: "Ready", loading: "Working…", success: "Done", error: "Something went wrong", } satisfies Record<Status, string>;
Add a new Status and this object fails to compile until you add its label. That pairs well with discriminated unions.
Framework configs
Many config files export an object with a known shape:
import type { NextConfig } from "next"; export default { output: "standalone", images: { remotePatterns: [{ hostname: "cdn.example.com" }] }, } satisfies NextConfig;
You get autocomplete and typo-checking without widening anything.
When not to use it
- Function parameters and return types. Annotate them. They're a contract for callers, and you want the declared type there.
- Values that will be reassigned to other members of the type.
let theme = {...} satisfies Themekeeps the narrow type, so assigning a different validThemelater can fail. Use an annotation for mutable variables. - Public API surfaces where you deliberately want to hide implementation details behind a wider type.
Cheat sheet
| You want | Use |
|---|---|
| "This variable is of type T, full stop" | const x: T = … |
| "Check this fits T, keep the precise type" | const x = … satisfies T |
| "Readonly literals, checked against T" | const x = … as const satisfies T |
| "I know better than the compiler" | … as T (rarely) |