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.primaryto be a literal'#3b82f6', not juststring. - 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
kindvalues for narrow switch-case typing. - React component prop maps —
as const satisfiesa 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
satisfiesdoesn’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
satisfiesresult in a widely-typed binding re-widens it — keep the narrow type by usingtypeofor passing directly. satisfiescan mask gentle widening fromas const-less code — combine withas constfor literal locking.- Excess property checks still apply — adding a key not in
Tis still an error. - Order matters:
as const satisfies Tis 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
as constand literal types: 01_generics.md, 03_template_literal_types.md- Why
asis dangerous: 10_typescript_pitfalls.md