Frontend / Typescript / 06_satisfies_operator.md

The satisfies Operator

Updated 5 interview angles 5 min read source
On this page12
  1. What does satisfies do, and what does it replace?
  2. satisfies vs annotation vs as — when each?
  3. How does satisfies interact with as const?
  4. Where is satisfies most useful in real code?
  5. What’s the difference between value as const satisfies T and value satisfies T as const?
  6. Common mistake — using satisfies and then assigning to a T-typed variable?
  7. Can satisfies be used with function return types?
  8. Does satisfies work with generics?
  9. Gotchas
  10. Related
  11. Further reading
  12. Interview angle

The satisfies Operator

satisfies T (TS 4.9+) checks that a value conforms to type T without widening the value’s inferred type. It’s the right answer when you want both “validate against a contract” and “keep the narrow literal shape for downstream type inference.” It replaces a category of as casts and annotation hacks.

What does satisfies do, and what does it replace?

It checks the value against a type — like an annotation — but doesn’t widen the value’s inferred type, the way an annotation would.

ts
type Palette = Record<'red' | 'green' | 'blue', string | [number, number, number]>

// Annotation widens — you lose tuple-vs-string distinction:
const a: Palette = {
  red: 'red',
  green: [0, 255, 0],
  blue: '#00f',
}
// error — type is `string | [number, number, number]`
a.green.toUpperCase()

// satisfies keeps the precise inferred type:
const b = {
  red: 'red',
  green: [0, 255, 0],
  blue: '#00f',
} satisfies Palette

b.green.map(c => c)     // ok — TS knows green is the tuple
b.red.toUpperCase()     // ok — TS knows red is a string

You get the contract check and the narrow type. Before TS 4.9, this required as const + manual typing or repetitive duplication.

satisfies vs annotation vs as — when each?

Use Tool Why
You want compile-time validation against a shape and narrow inference satisfies the whole point
You want to widen / convert a value’s type intentionally annotation : T widens to T
You want to override TS because you “know better” (network parse, library type bug) as T last resort, unchecked
Constant value whose literal types matter as const + (optionally) satisfies freezes literal types

satisfies never lies; as can. Reach for satisfies first.

How does satisfies interact with as const?

They compose. as const freezes the literal types; satisfies validates the result.

ts
const routes = {
  home: '/',
  user: '/users/:id',
  org:  '/orgs/:slug',
} as const satisfies Record<string, `/${string}`>

type R = typeof routes
// = { readonly home: '/'; readonly user: '/users/:id'; readonly org: '/orgs/:slug' }

Compare to annotation, which would widen everything to string and lose the literal information you’d want for a typed router.

Where is satisfies most useful in real code?

  • Config objects — validate against a config schema while preserving the specific keys/values for downstream typing.
  • Theme tokens — Tailwind/MUI-style colour palettes where you want theme.colors.primary to be a literal '#3b82f6', not just string.
  • Route maps — keep route strings as literal types for routers that use them in infer-based path-param extraction.
  • Discriminated state machines — preserve the literal kind values for narrow switch-case typing.
  • React component prop maps — as const satisfies a record so each entry’s specific prop type is preserved.
ts
const buttons = {
  primary: { variant: 'primary', size: 'md' },
  danger:  { variant: 'danger',  size: 'sm' },
} as const satisfies Record<string, ButtonProps>

What’s the difference between value as const satisfies T and value satisfies T as const?

The second form doesn’t parse the way you might expect (as const is not applied here in a useful way). Standard idiom is value as const satisfies T — freeze first, then validate.

Common mistake — using satisfies and then assigning to a T-typed variable?

That re-widens. The narrow types are lost as soon as the value is stored in a variable typed as T.

ts
const palette = {
  red: 'red',
  green: [0, 255, 0],
} satisfies Palette

const stored: Palette = palette   // ← stored loses the narrow inference; same problem as a plain annotation

Keep using the typeof palette type, or pass palette directly rather than storing it in a widely-typed binding.

Can satisfies be used with function return types?

Yes — same idea, validate the return shape without widening it.

ts
function getConfig() {
  return {
    env: 'prod',
    region: 'us-east-1',
    flags: ['a', 'b'],
  } satisfies Config
}

const c = getConfig()
// c.env is 'prod' (literal), not the wider type from Config

Does satisfies work with generics?

Yes, and it’s particularly useful for fixing an inferred generic to a known constraint without losing the specific value type.

ts
function makeRoutes<T extends Record<string, `/${string}`>>(t: T): T {
  return t
}

const r = makeRoutes({
  home: '/',
  about: '/about',
} satisfies Record<string, `/${string}`>)

Gotchas

  • satisfies doesn’t change runtime behaviour — it’s purely a compile-time check.
  • TypeScript 4.9+ only — older codebases can’t use it; they use as const + extra annotations.
  • Storing the satisfies result in a widely-typed binding re-widens it — keep the narrow type by using typeof or passing directly.
  • satisfies can mask gentle widening from as const-less code — combine with as const for literal locking.
  • Excess property checks still apply — adding a key not in T is still an error.
  • Order matters: as const satisfies T is the standard idiom; the reverse is rarely useful.

Further reading

Interview angle 5

  • “Why is as dangerous?” - it is unchecked. as tells the compiler to stop asking, so it is a runtime assumption wearing a type’s clothes, and it is a routine source of production bugs where the value never had that shape.
  • “So what does satisfies do instead?” - it validates the value against a type and keeps the narrow inferred type. An annotation would check it and then widen everything to the annotation; as would keep the narrow type and check nothing. satisfies is the case both of those got wrong.
    • Follow-up: “Give a concrete win.” - a config object, a theme token map, a route table, a state-machine definition. You want keyof of the actual keys and the literal value types, and you also want a typo in a value to be an error. Annotating loses the keys; as loses the checking.
  • “How does it interact with as const?” - they compose, and the order is fixed: value as const satisfies T. as const locks the literals, then satisfies checks the locked value against the type. The reverse order is almost never what you want.
  • “You used satisfies and lost the narrow type anyway. What happened?” - you assigned the result into a binding annotated with the wide type, which re-widens it immediately. Keep it narrow by using the value directly or by taking typeof of the satisfies expression.
  • “Does it change anything at runtime?” - no, it is purely a compile-time check and erases completely. It also needs TypeScript 4.9 or later, so older codebases fall back to as const plus a separate annotation.