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
satisfiesvalidates expressions match a type without widening- Use it when you want both validation AND preserved inference
- Replaces many uses of
astype assertions - Ideal for configuration objects, event maps, route definitions
- Combine with
as constfor readonly + validated + literal types
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.