satisfies is an operator that validates that a given value x is assignable to a given type A, without casting x to A. That distinction (validating rather than casting) is what makes it interesting, and it's why it does more than one useful thing.
The obvious use: catching drift
Say you have a shared type used by both the backend and the frontend:
// shared
type SomeInput = { data: number }
The backend uses it directly:
router.get('/api', (input: SomeInput) => { ... })
The frontend has a general-purpose fetch that takes anything. Pretend for a moment that making fetch generic over INPUT isn't an option, otherwise this example is moot:
function fetch(x: AnyObject): unknown { ... }
fetch({ data: 123 })
Everything compiles. Everything works. Now the shared type changes:
type SomeInput = { data: string }
The frontend call site is still valid TypeScript: fetch accepts AnyObject, and { data: 123 } is a perfectly fine object. The drift slides past the compiler and lands in production.
That's exactly what satisfies is for:
fetch({ data: 123 } satisfies SomeInput)
Now the compiler validates the literal against SomeInput at the call site. When the shared type changes to { data: string }, this line stops compiling. You get the error where the mismatch actually is, and the type of the argument passed to fetch is still the literal you wrote, with no cast and no widening.
The less-obvious use: as const satisfies
There's another shape of this operator that I find myself reaching for occasionally. It's niche, but when it fits, nothing else quite does the job.
Here's the setup:
enum SomeEnumType {
field = 0,
node = 1,
crap = 2,
}
function process(validEnumMember: [SomeEnumType.field, SomeEnumType.node]): void { ... }
const values = [SomeEnumType.field, SomeEnumType.node]
process(values) // error
The call fails because values is inferred as SomeEnumType[]. TypeScript widens the array literal from the specific pair to "any array of enum members." Reasonable in general, wrong for this call.
The natural next attempt is as const:
const values = [SomeEnumType.field, SomeEnumType.node] as const
process(values) // still an error
Now the type is readonly [SomeEnumType.field, SomeEnumType.node], and the parameter expects a mutable tuple. So we've swapped one error for another. The solution is to combine the two:
const values = [SomeEnumType.field, SomeEnumType.node] as const satisfies SomeEnumType[]
process(values) // works
What's happening here is that TypeScript recontextualizes the same value into a different type via inference. The as const narrows the literals so they don't widen to SomeEnumType, and the satisfies SomeEnumType[] gives the compiler enough context to drop the readonly when inferring the final type. Same value, different type. Narrow enough for the tuple, mutable enough for the parameter.
It's a genuinely strange corner of the language, and I don't have a clean mental model for why it works, only that it does.
Even weirder: satisfies alone prevents widening
In most real-world versions of the example above, you can drop the as const entirely. If process were slightly less inane and took a union array instead of a tuple:
function process(validEnumMember: (SomeEnumType.field | SomeEnumType.node)[]): void { ... }
Then this just works:
const values = [SomeEnumType.field, SomeEnumType.node] satisfies SomeEnumType[]
process(values)
For some reason, the presence of satisfies SomeEnumType[] stops TypeScript from widening the array literal to SomeEnumType[]. The inferred type of values ends up as the narrower (SomeEnumType.field | SomeEnumType.node)[], which is what process wants.
You would think that asking the compiler to check assignability against SomeEnumType[] would encourage it to widen, not the opposite. But satisfies preserves the literal type of the expression while validating the constraint, and here that preservation is exactly what saves the call site.
Wrapping up
Two things worth remembering:
satisfiesis the right tool for catching drift between a shared type and a permissive call site.- Combined with
as const, or sometimes used alone, it also nudges TypeScript's inference away from widening in cases where you want the narrow type preserved.
Crazy language. But it's family, and you love family for their idiosyncrasies.