TypeScript's satisfies Keeps Your Literals Alive

The satisfies operator type-checks your object literals without widening them to string. Keep exact types, key autocomplete, and typo checks.

Share

You wrote { path: "/admin" }, annotated the variable as Record<string, Route>, and TypeScript happily sanded your literal down to string. The precision you typed by hand is gone the moment the annotation touches it.

Since TS 4.9 there's an operator that checks a value against a type without erasing its inferred shape: satisfies. Most codebases I see use it exactly zero times.

Why this matters

Literal types are currency. They drive autocomplete on object keys, exhaustive switch checks, and narrower function returns. A plain annotation trades all that away for one broad check.

The usual workaround, as const, has the opposite problem: it keeps precision but checks nothing. A route with auth: "yes" compiles fine under as const. You've been choosing between safety and precision when you could have both.

How it works

expr satisfies T tells the compiler to verify expr is assignable to T, then keeps the type the expression would have inferred on its own. No widening, no relaxation.

Because the object literal is still "fresh" at that point, you also get typo detection on keys and values. And keyof typeof on the result gives you an exact key union, which is where the real payoff lives.

Where this helps

  • Route maps: keyof typeof routes becomes a literal union, so navigation code gets autocomplete and a missed case in a switch fails to compile.
  • Event handler registries: dispatch tables checked against a handler interface, with exact event names preserved for callers.
  • Test fixtures: fixture satisfies User catches shape drift without blurring the values your assertions depend on.
  • i18n tables: every locale entry validated against a message shape, keys stay a literal union for your t() wrapper.

Watch out

satisfies is compile-time only. It verifies the literal you wrote, not data from outside — JSON.parse, env vars, and req.body still need a real validator at the boundary.

It needs TS 4.9+, which is a non-issue in 2026 unless you're maintaining a fossil monorepo. Combining as const satisfies T makes everything readonly, so your target type must accept readonly shapes. And don't sprinkle it on every object — reach for it when you actually need both the check and the precision.

Try it yourself

Drop this in a file and run npx tsc --noEmit, or paste it into the TS playground:

type Route = { path: string; auth: boolean };

const routes = {
  home:  { path: "/",      auth: false },
  admin: { path: "/admin", auth: true  },
} satisfies Record<string, Route>;

type RouteName = keyof typeof routes; // "home" | "admin"

routes.admin.path;   // type "/admin" — literal survived
routes.admin.authh;  // error: 'authh' does not exist

// Compare: this is what your annotation was doing to you
const widened: Record<string, Route> = routes;
widened.admin.path;  // type string — precision gone

TL;DR

  • Type annotations widen your literals; as const skips checking; satisfies does both jobs.
  • Exact key unions and literal types give you autocomplete and exhaustiveness for free.
  • Swap one Record annotation in your codebase for satisfies today and watch what the compiler suddenly catches.