TypeScript 4.9: The satisfies Operator Explained

TypeScript 4.9 introduces the satisfies operator—a new way to validate that expressions match a type without widening. This solves a long-standing tension between type inference and type checking. In this guide, I’ll explain when to use satisfies vs type annotations, common patterns, and migration strategies for existing codebases.

The Problem with Type Annotations

type Color = "red" | "green" | "blue";

// Using type annotation - type WIDENS to Color
const palette: Record<string, Color> = {
  primary: "red",
  secondary: "green"
};

// ❌ Error: 'primary' doesn't exist on type Record<string, Color>
palette.primary.toUpperCase();

// We lost the knowledge that palette.primary is specifically "red"

The type annotation Record<string, Color> is correct, but it loses the specific keys. TypeScript knows palette has string keys, but not which specific keys.

The satisfies Solution

type Color = "red" | "green" | "blue";

// Using satisfies - type PRESERVES literal types
const palette = {
  primary: "red",
  secondary: "green"
} satisfies Record<string, Color>;

// ✅ Works! TypeScript knows palette.primary is "red"
palette.primary.toUpperCase();

// ✅ Type error if we add invalid color
const badPalette = {
  primary: "red",
  secondary: "purple" // ❌ Error: "purple" is not assignable to Color
} satisfies Record<string, Color>;

satisfies vs as const

// as const makes everything readonly and narrows to literals
const config = {
  port: 3000,
  host: "localhost"
} as const;
// Type: { readonly port: 3000; readonly host: "localhost" }

// satisfies validates but doesn't change the type
const config2 = {
  port: 3000,
  host: "localhost"
} satisfies { port: number; host: string };
// Type: { port: number; host: string }

Use as const when you want literal types. Use satisfies when you want validation without type widening.

Real-World Pattern: Configuration Objects

interface RouteConfig {
  path: string;
  handler: () => void;
  middleware?: (() => void)[];
}

// Before: Type annotation loses specific paths
const routes: Record<string, RouteConfig> = {
  home: { path: "/", handler: homeHandler },
  about: { path: "/about", handler: aboutHandler }
};

// routes.home - TypeScript doesn't know "home" exists

// After: satisfies preserves keys
const routes = {
  home: { path: "/", handler: homeHandler },
  about: { path: "/about", handler: aboutHandler }
} satisfies Record<string, RouteConfig>;

// routes.home - TypeScript knows this exists!
// routes.nonexistent - Type error

Pattern: Type-Safe Event Maps

type EventHandler<T> = (data: T) => void;

interface EventMap {
  userCreated: EventHandler<{ userId: string; email: string }>;
  orderPlaced: EventHandler<{ orderId: string; total: number }>;
}

const handlers = {
  userCreated: (data) => console.log(data.email), // data is typed!
  orderPlaced: (data) => console.log(data.total)  // data is typed!
} satisfies EventMap;

// TypeScript infers parameter types from satisfies constraint

Key Takeaways

  • satisfies validates expressions match a type without widening
  • Use it when you want both validation AND preserved inference
  • Replaces many uses of as type assertions
  • Ideal for configuration objects, event maps, route definitions
  • Combine with as const for readonly + validated + literal types

Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.