TypeScriptThe satisfies Operator

The satisfies Operator

Introduced in TypeScript 4.9, the satisfies operator lets you validate that a value conforms to a type without changing the inferred type of the variable. It is the best of both worlds: compile-time shape checking with full literal-type precision.

The Problem Before satisfies

Before satisfies, you had two unsatisfying options when you wanted type safety on an object literal:

TS
type Palette = Record<string, string | [number, number, number]>;

// Option 1: Annotate — validates the shape, but loses literal types
const palette: Palette = {
  red:  [255, 0, 0],
  blue: "#0000ff",
};
// palette.blue is now 'string | [number, number, number]'
// We can't call .toUpperCase() directly — TypeScript doesn't know it's a string

// Option 2: No annotation — keeps literal types, but no shape validation
const palette2 = {
  red:  [255, 0, 0],
  blue: "#0000ff",
};
// palette2.blue is 'string' — but there is no shape contract
// A typo like 'rede' instead of 'red' would go undetected
Note
Both options force a trade-off. Annotations give contracts but widen types. No annotations keep precision but lose contracts.
The satisfies Solution

satisfies checks that the value matches the type without changing the inferred type. The expression type remains exactly what TypeScript would normally infer.

TS
type Palette = Record<string, string | [number, number, number]>;

const palette = {
  red:  [255, 0, 0],
  blue: "#0000ff",
} satisfies Palette;

// TypeScript validates the shape: ✓  each value is string or [n,n,n]
// TypeScript keeps the precise types:
// palette.red  → [number, number, number]  (not string | [n,n,n])
// palette.blue → string                    (not string | [n,n,n])

// Now these work:
const [r, g, b] = palette.red;            // array destructuring on red
const upper     = palette.blue.toUpperCase(); // string method on blue
Success
With satisfies, you get contract validation AND retain the full precision of literal/inferred types — no trade-off.
satisfies vs. Type Annotation

Feature

Type annotation (:)

satisfies

Validates shape

Yes

Yes

Keeps inferred/literal types

No — widens to declared type

Yes — preserves inferred type

Allows extra properties

No (excess property check)

No (excess property check)

Can be used mid-expression

No

Yes

Changes the variable type

Yes

No

TS
type Config = { host: string; port: number };

// Annotation: variable type becomes Config
const c1: Config = { host: "localhost", port: 3000 };
// c1.host is 'string' (not "localhost")
// c1.port is 'number' (not 3000)

// satisfies: variable type stays precise
const c2 = { host: "localhost", port: 3000 } satisfies Config;
// c2.host is 'string' (same here — "localhost" is widened by let/const anyway)
// c2.port is 'number'

// More powerful with literal types
type Mode = "development" | "production";
type BuildConfig = { mode: Mode; sourceMap: boolean };

const config = {
  mode: "development",   // inferred as "development" literal
  sourceMap: true,
} satisfies BuildConfig;

type InferredMode = typeof config.mode; // "development" (not Mode)
satisfies with as const

Combining as const and satisfies is extremely powerful: as const makes types narrowest-possible, and satisfies validates the shape at the same time.

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

const ROUTES = {
  home:    { path: "/",       auth: false },
  profile: { path: "/profile", auth: true },
  admin:   { path: "/admin",   auth: true },
} as const satisfies Routes;

// Each path is a literal type (not just string)
type HomePath = typeof ROUTES.home.path; // "/"

// TypeScript validates: each value has path: string and auth: boolean  ✓
// ROUTES.missing = {} // Error — can't add properties to const
Tip
The order matters: write as const satisfies Type (const before satisfies). This first freezes the types to literals, then validates against the schema.
Validating Configuration Objects

satisfies shines for configuration objects where each key has a specific type but you want per-key type precision downstream.

TS
type LogLevel = "debug" | "info" | "warn" | "error";

type AppConfig = {
  port: number;
  logLevel: LogLevel;
  features: Record<string, boolean>;
  database: { url: string; poolSize: number };
};

const appConfig = {
  port: 8080,
  logLevel: "info",
  features: {
    darkMode: true,
    betaSearch: false,
  },
  database: {
    url: "postgres://localhost/mydb",
    poolSize: 10,
  },
} satisfies AppConfig;

// Precise types preserved:
appConfig.port;                    // number (8080 widens to number in let/const)
appConfig.logLevel;                // "info" (literal)
appConfig.features.darkMode;       // boolean
appConfig.database.url;            // string

// This would error (validation):
// const bad = { port: "8080" } satisfies AppConfig; // Error: string not assignable to number
satisfies in Discriminated Unions

TS
type Widget =
  | { kind: "button"; label: string; disabled?: boolean }
  | { kind: "input";  placeholder: string; type: "text" | "email" }
  | { kind: "checkbox"; checked: boolean; label: string };

// Validate each widget has the right shape, preserve literal kind
const widgets = [
  { kind: "button",   label: "Submit",      disabled: false },
  { kind: "input",    placeholder: "Email", type: "email"   },
  { kind: "checkbox", checked: false,       label: "Agree"  },
] satisfies Widget[];

// Each element has precise types:
const first = widgets[0];
// first.kind is "button" (literal), not the union
// first.label is string, first.disabled is boolean
satisfies with Function Return Values

TS
interface Handler {
  path: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
  handler: (req: Request) => Response;
}

// Validate the array of handlers at definition time
const routes = [
  {
    path: "/api/users",
    method: "GET",
    handler: (req: Request) => new Response("OK"),
  },
  {
    path: "/api/users",
    method: "POST",
    handler: (req: Request) => new Response("Created", { status: 201 }),
  },
] satisfies Handler[];

// routes[0].method is "GET" (literal), not "GET" | "POST" | "PUT" | "DELETE"
Error Catching with satisfies

The key benefit of satisfies is catching shape errors at the point of definition, not at use:

TS
type Color = { r: number; g: number; b: number };
type Theme = Record<"primary" | "secondary" | "accent", Color>;

// Error caught immediately — 'accent' key is missing
const myTheme = {
  primary:   { r: 255, g: 0,   b: 0   },
  secondary: { r: 0,   g: 255, b: 0   },
  // accent is missing — satisfies catches this
} satisfies Theme;
//  ^^^^^^^^^^^^^^ Error: Property 'accent' is missing

// Typo in property name
const colors = {
  pirmary: { r: 0, g: 0, b: 255 }, // typo: 'pirmary' not 'primary'
} satisfies Partial<Theme>;
// Error: Object literal may only specify known properties, 'pirmary' doesn't exist
Warning
Without satisfies (or a type annotation), both of the above errors would silently compile and only appear as runtime bugs.
satisfies vs. Type Annotation vs. as const — Cheat Sheet

TS
type Theme = { primary: string; secondary: string };

// 1. Type annotation — validates shape, widens types
const t1: Theme = { primary: "#f00", secondary: "#00f" };
// t1.primary: string  (not "#f00")

// 2. as const — narrows to literals, no validation
const t2 = { primary: "#f00", secondary: "#00f" } as const;
// t2.primary: "#f00"  — but no check that it matches Theme

// 3. satisfies — validates AND keeps narrow types
const t3 = { primary: "#f00", secondary: "#00f" } satisfies Theme;
// t3.primary: string  (string literal widens because const doesn't freeze it)

// 4. as const satisfies — narrows AND validates  ← usually what you want
const t4 = { primary: "#f00", secondary: "#00f" } as const satisfies Theme;
// t4.primary: "#f00"  — literal AND validated against Theme
Note
When in doubt, reach for as const satisfies YourType. It gives you the strictest type safety in both directions: the shape is validated against the contract, and the inferred type retains full literal precision.
Design Tokens and Theme Systems

satisfies is especially valuable in design systems where you want to enforce that all theme tokens are present and correctly typed, while retaining literal types for downstream code generation.

TS
type SpacingScale = Record<"xs" | "sm" | "md" | "lg" | "xl", number>;
type ColorPalette = Record<string, string>;

const spacing = {
  xs:  4,
  sm:  8,
  md: 16,
  lg: 24,
  xl: 32,
} as const satisfies SpacingScale;
// All five keys required; values remain literal numbers (4, 8, 16, ...)

type ColorToken = "brand" | "accent" | "neutral" | "error" | "success";
type Colors = Record<ColorToken, string>;

const colors = {
  brand:   "#3b82f6",
  accent:  "#8b5cf6",
  neutral: "#6b7280",
  error:   "#ef4444",
  success: "#22c55e",
} as const satisfies Colors;

// Derive the CSS variable keys at type level
type ColorVar = `--color-${keyof typeof colors}`;
// "--color-brand" | "--color-accent" | "--color-neutral" | ...
satisfies with Readonly Arrays

satisfies works on arrays too — validating that every element matches the schema while preserving tuple-level or element-level types.

TS
type Env = "development" | "staging" | "production";
type EnvConfig = { name: Env; url: string; debug: boolean };

const environments = [
  { name: "development", url: "http://localhost:3000", debug: true  },
  { name: "staging",     url: "https://staging.example.com", debug: true  },
  { name: "production",  url: "https://example.com",         debug: false },
] satisfies EnvConfig[];

// Each element keeps its literal types:
const devName = environments[0].name; // "development"  (not Env)

// Combining with as const:
const envs = [
  { name: "development", url: "http://localhost:3000", debug: true  },
  { name: "production",  url: "https://example.com",  debug: false },
] as const satisfies EnvConfig[];

type ProdUrl = typeof envs[1]["url"]; // "https://example.com"  (literal)
satisfies with Interface Hierarchies

satisfies also works when the validating type is an interface or a complex structural type — not just a Record. This is useful when you have a base contract with optional extension points.

TS
interface Plugin {
  name: string;
  version: string;
  install(app: unknown): void;
  uninstall?(): void;
}

// Validates that each plugin matches the Plugin interface
// while keeping the literal 'name' and 'version' types
const routerPlugin = {
  name: "router",
  version: "1.2.0",
  install(app: unknown) { console.log("Router installed"); },
} satisfies Plugin;

const storePlugin = {
  name: "store",
  version: "2.0.1",
  install(app: unknown) { console.log("Store installed"); },
  uninstall() { console.log("Store uninstalled"); },
} satisfies Plugin;

// routerPlugin.name is "router" (literal), not string
type RouterName = typeof routerPlugin.name; // "router"
Common satisfies Patterns Summary

Pattern

Example

Benefit

Config object

config satisfies AppConfig

Validates all required fields exist

as const satisfies

{ mode: "dark" } as const satisfies Theme

Literal types + shape validation

Route/handler map

routes satisfies Handler[]

Exhaustive route type checking

Design tokens

colors as const satisfies Colors

Token completeness + literal values

Discriminated union array

actions satisfies Action[]

Per-action field checking

Plugin registry

plugin satisfies Plugin

Interface compliance + literal name

satisfies in Library Code

When writing a library or framework, satisfies lets you validate internal implementation objects against exported contracts without losing internal precision. This is especially important when the implementation needs to derive types from the object itself.

TS
// A mini plugin framework
interface PluginContract {
  name: string;
  hooks: {
    onInit?: () => void;
    onDestroy?: () => void;
  };
}

// Internal registry — validate contract but keep literal name types
const BUILT_IN_PLUGINS = {
  logger: {
    name: "logger",
    hooks: {
      onInit:    () => console.log("Logger init"),
      onDestroy: () => console.log("Logger destroyed"),
    },
  },
  metrics: {
    name: "metrics",
    hooks: {
      onInit: () => console.log("Metrics init"),
    },
  },
} as const satisfies Record<string, PluginContract>;

// BUILT_IN_PLUGINS.logger.name is "logger" (literal), not string
// Useful for routing, plugin lookup, code splitting, etc.
type BuiltInPluginName = keyof typeof BUILT_IN_PLUGINS;
// "logger" | "metrics"
Summary
  • satisfies validates that a value matches a type without widening the inferred type

  • Unlike a type annotation, satisfies preserves literal and per-property types

  • as const satisfies Type freezes literals AND validates the schema in one expression

  • Ideal for configuration objects, route tables, design tokens, and enum-like maps

  • Catches shape errors (missing keys, wrong types) at the point of definition

  • Available from TypeScript 4.9 onward

  • Combines naturally with discriminated unions to preserve the literal discriminant type

  • Works on arrays too — validates each element while preserving tuple-level literal types

  • In library code, validates implementation objects against contracts while retaining literal precision

  • In design systems, it enforces complete token coverage while keeping literal precision