For years, TypeScript gave you two bad options when you wanted to check that a value conformed to a type: annotate the variable and lose the precise literal types, or skip the annotation and lose the type checking. satisfies, added in TypeScript 4.9, closes that gap. It validates a value against a type without changing the type TypeScript infers for it.

The problem it fixes

Say you're building a routing config where each route has a path and a component loader. You want TypeScript to catch typos in the shape, but you also want to keep the literal keys so you can do routes.home with autocomplete instead of indexing with a string.

typescript
type RouteConfig = Record<string, { path: string; auth?: boolean }>;

// Annotated: shape is checked, but literal keys are gone
const routesAnnotated: RouteConfig = {
  home: { path: "/" },
  settings: { path: "/settings", auth: true },
};

// routesAnnotated.home  // error: string index signature only

Because routesAnnotated is typed as RouteConfig, TypeScript widens every property to { path: string; auth?: boolean } accessed via a string index. You lose home and settings as distinct, known keys. Switch to satisfies and the object keeps its own inferred shape while still being checked against RouteConfig.

typescript
const routes = {
  home: { path: "/" },
  settings: { path: "/settings", auth: true },
} satisfies RouteConfig;

routes.home.path;      // "/" — literal key access works
routes.settings.auth;  // boolean, and TypeScript still verified the shape

If you typo a key name that isn't path or auth, or give path a number, satisfies still flags it — you get the same error you'd get from an annotation. The difference is purely about what type gets recorded for the variable afterward.

satisfies vs as const

as const locks a value down to its narrowest possible literal type but does zero structural checking. You can write { path: 123 } and TypeScript won't complain, because as const isn't validating against any target type — it's just freezing what's there.

typescript
const badRoute = { path: 123 } as const; // compiles, but path should be a string

satisfies and as const aren't mutually exclusive, either. Combine them when you want both a narrowed literal type and a structural check:

typescript
const theme = {
  primary: "#1e293b",
  secondary: "#64748b",
} as const satisfies Record<"primary" | "secondary", string>;

Here TypeScript confirms the object has exactly primary and secondary keys with string values, and as const still gives you the literal string types for each, rather than widening them to string.

Where it actually earns its keep

The sweet spot is any object literal you both want validated and want to keep working with by its own precise shape afterward — config objects, permission maps, enum-like lookup tables, or default option objects passed into a function that also needs autocomplete on the individual keys.

It's not a replacement for interfaces on function parameters or return types — those still benefit from explicit annotations because callers need a stable contract, not the literal type of whatever you happened to write inline. Reach for satisfies specifically when the value is being defined and consumed in the same file or module, and the literal shape matters downstream. For anything crossing a module boundary, an exported type still does more work than an inferred one.

One caveat: satisfies doesn't excess-property-check the same way object literal assignment does in every case, so don't assume it catches every stray property the way assigning directly to an interface-typed variable would — test the specific pattern you're relying on before trusting it blindly.