Skip to content
TypeScript ts assertions 4 min read

The satisfies Operator

The satisfies operator, added in TypeScript 4.9, checks that an expression conforms to a given type without changing the type the compiler infers for it. It solves a long-standing tension: a type annotation validates your value but throws away its specific shape, while a type assertion preserves a shape but skips validation. satisfies gives you both — full constraint checking against the target type, plus the narrow, literal-rich type that inference would naturally produce. It is the right tool whenever you want a value to match a contract while still accessing its precise members.

The problem it solves

Consider a palette object that must have string values, but where you also want to use each specific color. An annotation enforces the constraint but erases the specifics.

type Palette = Record<string, string>;

const colors: Palette = {
  primary: "#0070f3",
  danger: "#e00",
};

colors.primary.toUpperCase(); // ✅ string method
// ❌ Error: Property 'toUpperCase' is fine, but...
// colors.purple — no error! Record<string, string> allows any key

With the annotation, colors is typed as Record<string, string>, so the compiler forgets that only primary and danger exist and lets you read a non-existent colors.purple as string. The literal value information is gone.

How satisfies keeps the narrow type

Replacing the annotation with satisfies validates the same constraint but leaves the inferred type intact.

type Palette = Record<string, string>;

const colors = {
  primary: "#0070f3",
  danger: "#e00",
} satisfies Palette;

colors.primary.toUpperCase(); // ✅ still a string
// ❌ Error: Property 'purple' does not exist on type '{ primary: string; danger: string; }'.
// colors.purple;

Now colors keeps its exact inferred type { primary: string; danger: string }. You still get the Palette guarantee — every value must be a string — but the compiler also remembers which keys actually exist, catching the typo on purple.

Catching value mismatches

Because satisfies runs a real compatibility check, it flags values that violate the constraint — something an assertion would silently allow.

type RGB = [number, number, number];
type ColorMap = Record<string, RGB | string>;

const theme = {
  bg: [255, 255, 255],
  fg: "#000",
  // ❌ Error: Type 'string' is not assignable to type 'RGB | string'... actually:
  accent: [0, 128], // ❌ Error: not a 3-tuple
} satisfies ColorMap;

The error points exactly at accent, where the tuple has the wrong length. An as ColorMap cast would have accepted it without complaint, deferring the failure to runtime.

satisfies vs annotation vs assertion

These three are easy to confuse. The distinction comes down to what gets checked and what type survives.

FormValidates value?Resulting typeUse when
const x: T = valueYesT (widened)You want the variable typed as the contract
const x = value as TNo (override)TYou know more than the compiler
const x = value satisfies TYesinferred (narrow)You want validation and precise members
const a: string[] = ["a", "b"];          // type: string[]
const b = ["a", "b"] as const;            // type: readonly ["a", "b"]
const c = ["a", "b"] satisfies string[];  // type: string[] (here, same as inferred)

The headline: an annotation gives you the broad type, an assertion bypasses checking entirely, and satisfies checks while preserving whatever the value actually is.

Combining satisfies with as const

The two operators compose beautifully. Use as const to lock in literals, then satisfies to assert the locked value still matches a contract.

type RouteConfig = Record<string, { path: string; auth: boolean }>;

const routes = {
  home: { path: "/", auth: false },
  dashboard: { path: "/dashboard", auth: true },
} as const satisfies RouteConfig;

type RouteKey = keyof typeof routes; // "home" | "dashboard"
routes.dashboard.auth; // type: true (literal, not boolean)

Order matters: write as const satisfies T. The as const narrows first, then satisfies verifies the narrowed value fits T. This pattern gives you immutable, literal-typed config that is also provably correct against its schema.

When to use each tool

Reach for satisfies as the default for typed constant objects — config, route maps, theme tokens, command registries — anywhere you want both a guarantee and rich autocomplete. Use a plain annotation when you genuinely want the variable to be the wider type (for example, a function parameter or a value you will reassign). Reserve as for the rare case where you truly know more than the compiler.

// ✅ config object — satisfies
const settings = { retries: 3, debug: false } satisfies AppSettings;

// ✅ you want the broad type for later reassignment — annotation
let handler: RequestHandler = defaultHandler;

// ⚠️ DOM element the compiler can't infer — assertion
const canvas = document.getElementById("c") as HTMLCanvasElement;

Best Practices

  • Default to satisfies for typed constant objects so you get validation and precise inferred types.
  • Use as const satisfies T (in that order) for immutable, literal-typed, schema-checked config.
  • Prefer satisfies over a type annotation when you need to access specific keys or literal values.
  • Prefer satisfies over as whenever possible — it verifies the value instead of overriding the compiler.
  • Use a plain annotation when you actually want the broader contract type (parameters, reassignable vars).
  • Derive helper types from the inferred value with keyof typeof and (typeof x)[...].
  • Remember satisfies is compile-time only and emits no runtime code.
Last updated June 29, 2026
Was this helpful?