Skip to main content

The satisfies operator in TypeScript

satisfies - checks if a value matches a type at compile time while keeping the inferred narrow type intact. TypeScript 4.9 added it to fix one specific annoyance: : Type annotation widens your value to the broad type, erasing the specific string, array shape, or literal that TypeScript already knows about.

Theory

TL;DR

  • : Type assigns the broad type to your variable and loses details; satisfies Type validates against the type but keeps the narrow inference
  • Analogy: type annotation reshapes your value to fit the declared type; satisfies checks the shape without reshaping it
  • Use satisfies when you need validation AND still want to call .toUpperCase() on a string or .map() on a specific array afterward
  • Zero runtime cost - fully erased in JS output
  • Decision rule: object with mixed value types that you need to access specifically after validation? Use satisfies

Quick example

typescript
type Colors = Record<string, string | number[]>; // Type annotation: widens to string | number[], loses specifics const colors: Colors = { red: "#ff0000", green: [0, 255, 0] }; colors.red.toUpperCase(); // ❌ string | number[] has no toUpperCase // satisfies: validates, then keeps narrow inference const colors2 = { red: "#ff0000", green: [0, 255, 0] } satisfies Colors; colors2.red.toUpperCase(); // ✅ TypeScript infers string colors2.green.map(x => x * 2); // ✅ TypeScript infers number[]

The type check runs, catches any mismatch, and then TypeScript uses the narrow inferred type for everything that follows.

Key difference

: Type assigns Type to the variable. From that point, TypeScript sees only string | number[] and forgets that red was specifically a string. satisfies runs the same check but substitutes the original narrow inference back into the declaration. Same compile-time safety, different result downstream.

When to use

  • Object needs validation, but you access specific properties after: use satisfies
  • as const is too narrow (readonly literals) and you want flexible validation: use satisfies
  • Config objects, theme maps, route tables, status code constants: all good fits
  • Simple primitives (5 satisfies number): skip it, inference already handles this
  • Object grows beyond 5 properties and splitting into individually typed fields gets messy: use satisfies over annotations

Comparison table

ApproachValidates?Preserves narrow type?Notes
const x: Type = valueYesNo (widens to Type)Standard annotation
const x = value as TypeNoNoSkips all checks
const x = { ... } as constNoYes (readonly)Too narrow for most cases
const x = { ... } satisfies TypeYesYesValidation + inference
When to useComputed values, return typesSimple readonly literalsComplex validated objects

How the compiler handles this

TypeScript's type checker assigns the inferred type of the value to a temporary node, verifies it's assignable to the target type, then puts the original narrow inference back into the declaration. No runtime code is generated. It relies on structural subtyping, the same mechanism as implements on a class. The emitted JavaScript is identical to what you'd get without satisfies.

Common mistakes

Expecting satisfies to fill in missing properties

typescript
// Wrong: satisfies checks exact match, not intersection filling const x = {} satisfies { a: number } & { b: string }; // ❌ Error: missing a and b // Fix: provide all required properties const x = { a: 1, b: "hi" } satisfies { a: number } & { b: string }; // ✅

Using it on primitives

typescript
// Pointless - TypeScript already infers 5 as number const n = 5 satisfies number;

This adds no value and confuses anyone reading the code.

Expecting body narrowing on functions

typescript
// satisfies only checks the function signature, not the body const fn = ((x: string) => x.length) satisfies (x: string) => number; // The return type is checked, but closure variables inside aren't narrowed

If you need the function itself to be narrowed, use as const.

Nested objects losing inference depth

satisfies checks the top-level structure. Nested objects are validated but not always narrowed as deeply as you'd expect. One edge case: { children: [] } satisfies Tree infers children as never[] because an empty array has no element type to infer from. Apply satisfies closer to where you consume the value, or use a recursive type.

Real-world usage

  • Next.js: const metadata = { title: "App" } satisfies Metadata - validates static exports, keeps the literal string for og:title processing
  • TanStack Query: const defaultOptions = { queries: { retry: 3 } } satisfies DefaultOptions - preserves the number literal for override comparisons downstream
  • tRPC: validating router shapes while keeping procedure-level type inference intact
  • Zod: const config = { ... } satisfies z.infer<typeof schema> - shape check with literal defaults preserved

I've found this pattern most useful in config files with more than five properties. At that point, splitting into individually typed fields gets unwieldy, and as const forces readonly everywhere you don't want it.

Follow-up questions

Q: What's the difference between satisfies, as const, and type assertion?
A: satisfies validates and keeps narrow types. as const keeps narrow types without validation. Type assertion (as Type) skips validation entirely - it's a cast, not a check.

Q: Does satisfies have any runtime impact?
A: None. It's compile-time only, completely erased in the emitted JavaScript. The output is identical to a plain object literal.

Q: Can you combine satisfies with as const?
A: Yes: { ... } as const satisfies Type. This gives you readonly literal types plus compile-time validation against the target type. Order matters - as const first, then satisfies.

Q: How would you polyfill satisfies in a TypeScript 4.8 project?
A: Use a helper type: type Satisfies<T, U extends T> = U. Then write const x = { ... } as Satisfies<Colors, typeof x>. It's awkward but covers most cases before upgrading to 4.9+.

Q: Why not always use satisfies instead of annotations?
A: Annotations work for computed values, function return types, and cases where you actually want the broad type for downstream consumers. satisfies requires a literal value to infer from - it can't help you type a value that doesn't exist yet at the declaration site.

Examples

Basic: colors palette config

typescript
type Colors = Record<string, string | number[]>; const palette = { red: "#ff0000", green: [0, 255, 0], blue: "#0000ff", } satisfies Colors; // ✅ TypeScript knows red and blue are strings palette.red.startsWith("#"); // works palette.blue.toUpperCase(); // works // ✅ TypeScript knows green is number[] palette.green.map(channel => channel * 0.5); // works

With Colors as the annotation instead, all three properties become string | number[] and none of those method calls compile without a type guard first.

Intermediate: route configuration

typescript
type Route = { path: string; method: "GET" | "POST" | "PUT" | "DELETE"; handler: string; }; const routes = { getUsers: { path: "/api/users", method: "GET", handler: "UserController.getAll", }, createUser: { path: "/api/users", method: "POST", handler: "UserController.create", }, } satisfies Record<string, Route>; // TypeScript knows the exact keys type RouteKeys = keyof typeof routes; // "getUsers" | "createUser" // And the exact method literals routes.getUsers.method; // "GET", not "GET" | "POST" | "PUT" | "DELETE" // Validation still fires on typos const bad = { home: { path: "/", method: "PATCH", handler: "HomeController" }, } satisfies Record<string, Route>; // ❌ Error: "PATCH" is not assignable to "GET" | "POST" | "PUT" | "DELETE"

This pattern shows up in Express middleware setups and Next.js App Router configs. You get the type safety from the annotation without losing the literal types you need for routing logic or switch statements.

Advanced: combining with as const

typescript
const palette = { red: "#ff0000", green: "#00ff00", blue: "#0000ff", } as const satisfies Record<string, `#${string}`>; // Values are readonly literal types AND validated as hex color strings type PaletteKey = keyof typeof palette; // "red" | "green" | "blue" palette.red; // "#ff0000" (readonly literal, not just string) // Validation catches format violations at compile time const badPalette = { yellow: "ffff00", // Missing # } as const satisfies Record<string, `#${string}`>; // ❌ Error: "ffff00" is not assignable to `#${string}`

as const makes the values readonly literals. satisfies checks they match the target format. Together they give you a validated, fully typed constant map with zero runtime overhead.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?