TypeScript satisfies Operator

September 6, 202610 min read
Requirements:
Objects| UnionsGenerics

The TypeScript satisfies operator, added in 4.9, exists to solve a problem you have almost certainly hit without ever naming it. You want TypeScript to check that an object matches a shape, and you also want to keep the precise types it inferred from the literal you wrote. Before satisfies, those were two different requests and you could only make one of them.

The operator itself is one word in one position, so most of the work here is understanding what it replaces. That is easiest to see by watching the old approaches fail.

The problem

Here is a config object typed the normal way, with an annotation.

type ServerConfig = Record<string, string | string[]>
 
const annotated: ServerConfig = {
  host: 'localhost',
  ports: ['8080', '8081'],
}
 
// annotated.ports is string | string[] — TypeScript no longer knows it is an array
// annotated.ports.map((p) => p)
// ❌ Property 'map' does not exist on type 'string | string[]'

The annotation did its job: it checked the object. But it also replaced everything TypeScript had worked out about your actual values with the type you declared. ports is an array, you can see on the line above that it is an array, and TypeScript now insists it might be a string.

This is not a bug. A variable annotation is a declaration of what the variable is, and the value only has to be assignable to it. Once the declaration is made, the value's own type stops being interesting. That is what you want for a function parameter and what you almost never want for a config object.

Drop the annotation and the opposite failure shows up.

const inferred = {
  host: 'localhost',
  ports: ['8080', '8081'],
}
 
inferred.ports.map((p) => p) // ✅ the inferred type really is string[]
 
// But nothing is checking the shape any more:
const typo = {
  host: 'localhost',
  prots: ['8080'], // ✅ accepted — there is no type for it to fail against
}

Inference is now perfect and validation is gone. You have a config object that compiles happily with a misspelled key and breaks at runtime, in an app that is otherwise fully typed. The usual workaround was to annotate anyway and then sprinkle as casts at every read site, which trades one problem for a worse one.

What the TypeScript satisfies operator does

satisfies checks an expression against a type and then discards that type, keeping the inferred one.

// ServerConfig is the same type declared above
const checked = {
  host: 'localhost',
  ports: ['8080', '8081'],
} satisfies ServerConfig
 
checked.host.toUpperCase() // ✅ string
checked.ports.map((p) => p) // ✅ string[], not string | string[]

Both halves at once. The object is verified against ServerConfig, and checked.ports is still string[]. The type is being used as a constraint on the value rather than as a replacement for it.

The mental model that sticks: an annotation says "treat this value as a ServerConfig", while satisfies says "prove this value is a ServerConfig, then forget I asked". The check happens, the result is thrown away, and you keep the sharper type you started with.

Note where it goes. satisfies is a postfix operator on an expression, not a piece of a declaration, so it sits after the value and works anywhere an expression works — including inside a function argument or a return statement. That is a meaningful difference from an annotation, which can only attach to a declaration site. If you want to verify an object you are passing straight into a function call, satisfies is the only one of the three tools that can do it without first pulling the value out into a named variable.

One consequence is that satisfies never changes what your code does, only what the compiler accepts. Adding it to an existing object literal can turn up errors that were always there, but it cannot alter the emitted JavaScript or the value at runtime. That makes it a safe thing to add to code you do not fully trust yet: the worst case is that TypeScript tells you something you did not want to hear.

satisfies vs annotation vs as

There are three ways to point a type at a value, and they do genuinely different things.

type Route = { path: string; method: 'GET' | 'POST' }
 
// Annotation — checked, but the value takes on the declared type
const annotatedRoute: Route = { path: '/users', method: 'GET' }
// annotatedRoute.method is 'GET' | 'POST'
 
// as — no widening complaint, but no real check either
const assertedRoute = { path: '/users', method: 'GET' } as Route
// assertedRoute.method is 'GET' | 'POST', and a wrong shape would be papered over
 
// satisfies — checked, and the narrow inferred type survives
const satisfiedRoute = { path: '/users', method: 'GET' } satisfies Route
// satisfiedRoute.method is 'GET'

as is the dangerous one. It is an assertion, which means you are telling the compiler what to believe and it stops arguing. satisfies is the exact inverse: it asks the compiler to agree with you and fails loudly when it does not. If you have been reaching for as to quiet an error on an object literal, satisfies is almost always what you actually wanted, and it will usually reveal that the error was real.

The contextual typing is worth calling out separately. Because satisfies supplies a contextual type to the expression, array literals can be inferred as tuples rather than arrays:

type Palette = Record<'primary' | 'accent', string | [number, number, number]>
 
const palette = {
  primary: '#2563eb',
  accent: [37, 99, 235],
} satisfies Palette
 
palette.primary.toUpperCase() // ✅ string, no narrowing needed
palette.accent.map((channel) => channel / 255) // ✅ [number, number, number]

With a plain annotation, both properties would be string | [number, number, number] and every single access would need a type guard in front of it. This is the same widening friction that makes union types tedious to work with when you already know perfectly well which member you are holding.

Config objects and Record

The most common real-world use of satisfies is validating a lookup table against Record.

type Feature = 'search' | 'billing' | 'exports'
 
const featureOwners = {
  search: 'platform',
  billing: 'payments',
  exports: 'platform',
} satisfies Record<Feature, string>
 
featureOwners.search // ✅ autocomplete still knows the exact keys
// featureOwners.audit
// ❌ Property 'audit' does not exist on type '{ search: string; billing: string; exports: string; }'

Two things are true at once here, and neither is true without satisfies. Every key is checked against Feature, so a typo fails at the definition. And the object keeps its own key list, so autocomplete offers you three specific properties instead of accepting any string.

The bigger payoff arrives months later. Add a member to Feature and this object stops compiling until somebody fills in the missing entry:

type FeatureWithAudit = Feature | 'audit'
 
// const featureOwners = {
//   search: 'platform',
//   billing: 'payments',
//   exports: 'platform',
// } satisfies Record<FeatureWithAudit, string>
// ❌ Property 'audit' is missing in type '{ search: string; billing: string; exports: string; }'
//    but required in type 'Record<FeatureWithAudit, string>'

That is exhaustiveness checking on a plain object, for free, with no runtime code and no check anyone has to remember to write. The union of keys becomes the source of truth and every table built from it has to keep up with it.

as const satisfies

satisfies validates, but it does not freeze. The values inside the object still widen the way any object literal's values widen. If you want the literals preserved too, stack it on as const.

type Team = 'platform' | 'payments'
 
const teamOwners = {
  search: 'platform',
  billing: 'payments',
  exports: 'platform',
} as const satisfies Record<Feature, Team>
 
type SearchOwner = (typeof teamOwners)['search'] // 'platform'

The order matters and reads left to right: as const keeps the values as literal types, satisfies then proves those literals are valid Team values, and typeof lets you pull types back out of the object afterwards. Without as const, SearchOwner would just be string.

That trio is the modern replacement for a lot of what enums were reached for — a const object with as const satisfies gives you the same exhaustiveness and the same autocomplete, without emitting any runtime code and without the quirks of the enum type itself.

It also composes with template literal types when the keys follow a pattern:

type EventName = `user:${'created' | 'deleted'}`
 
const handlers = {
  'user:created': () => 'welcome',
  'user:deleted': () => 'goodbye',
} as const satisfies Record<EventName, () => string>
 
type Handled = keyof typeof handlers // 'user:created' | 'user:deleted'

Misspell an event key and the object fails against EventName right there at the definition, rather than silently never firing three files away at the call site.

Where satisfies does not help

It is a compile-time operator with a narrow job, and it is easy to over-read what it promises.

type Port = { value: number }
 
const port = { value: 8080 } satisfies Port
 
port.value = 9090 // ✅ still mutable — satisfies is not as const
 
// satisfies checks an expression, so it is not an annotation. This is not valid syntax:
// function listen(config satisfies Port) {}
 
// And it is erased, so it validates nothing about data you fetched:
// const data = (await response.json()) satisfies Port
// ❌ a claim about a value the compiler has never seen

Three things to keep straight:

There is also a taste question about where it belongs, and it is worth being deliberate. For a shared type used across many files, an annotation or an interface communicates intent better and gives you a single place to change. satisfies earns its keep on concrete values: config objects, lookup tables, route maps, theme definitions, event handler registries — the places where the literal you wrote is the information you want to keep, and where a widened type would throw away the only useful thing about it.

Summary

satisfies closes the gap between checking a value and inferring from it. An annotation checks and widens, as neither checks nor widens, and satisfies checks without widening. That one sentence covers most of the decisions you will make about it.

Reach for it when you are writing a concrete object literal and you want TypeScript to verify the shape without flattening the details — most often a config or lookup table validated against Record. Add as const in front of it when the literal values matter too, and keep using annotations and interfaces for the shared types that describe your domain rather than a specific value.

The official write-up is in the TypeScript 4.9 release notes.

Share this article

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges

Practice with Challenges

Put your typescript satisfies operator knowledge to the test with these related challenges.

#11Tuple to Object
Easy
#7Readonly
Easy
#4Pick
Easy
#34857Defined Partial Record
Medium

Related Concepts

Concepts that build on or relate to typescript satisfies operator.

TypeScript typeofUnion TypesTypeScript RecordInterfacesTypeScript Enums