frontend / typescript / 06_satisfies_operator.md

The satisfies Operator

4 min read source

The satisfies Operator

TL;DR

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.

Interview Q&A

Q: 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.

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',
}
a.green.toUpperCase()   // error — type is `string | [number, number, number]`

// 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.

Q: 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.

Q: How does satisfies interact with as const?

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

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.

Q: 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 mapsas const satisfies a record so each entry’s specific prop type is preserved.
const buttons = {
  primary: { variant: 'primary', size: 'md' },
  danger:  { variant: 'danger',  size: 'sm' },
} as const satisfies Record<string, ButtonProps>

Q: 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.

Q: 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.

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.

Q: Can satisfies be used with function return types?

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

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

Q: 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.

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

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

Gotchas / edge cases

  • 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.

What a senior is expected to say

A junior reaches for as when types misbehave. A senior knows that as is unchecked — a runtime assumption masquerading as a type, and a common bug source. satisfies does what as should have done for the “I want this validated but want the narrow inferred type” case. The senior cites concrete wins: config objects, theme tokens, route maps, state-machine variants — all places where annotation widens too much and as lies.

Cross-references

Further reading