TypeScript satisfies operator patterns for narrow inference and exhaustive switches. Use when defining typed objects that need both type validation and narrow type inference, or when building exhaustive switch/case handlers for union types.
satisfies validates that an expression matches a type without widening the inferred type. Prefer satisfies over type annotation (: Type) when you need per-key or per-value narrow inference.
Type annotation widens; satisfies preserves per-key shape.
type Route = { path: string; children?: Route[] };
// Bad: annotation widens -- TS only knows Record<string, Route>
const routes: Record<string, Route> = {
home: { path: '/home' },
admin: { path: '/admin', children: [{ path: '/users' }] },
};
routes.home.children; // Route[] | undefined -- can't tell home has no children
// Good: satisfies preserves per-key types
const routes = {
home: { path: '/home' },
admin: { path: '/admin', children: [{ path: '/users' }] },
} satisfies Record<string, Route>;
routes.home.children; // undefined (narrow)
routes.admin.children; // Route[] (narrow)
Union value narrowing works the same way:
type Palette = Record<string, string | number[]>;
// Bad: annotation widens every value to string | number[]
const palette: Palette = { red: '#ff0000', gradient: [255, 0, 0] };
palette.red.toUpperCase(); // Error: property doesn't exist on string | number[]
// Good: satisfies preserves the specific type per key
const palette = {
red: '#ff0000',
gradient: [255, 0, 0],
} satisfies Palette;
palette.red.toUpperCase(); // OK -- TS knows red is string
palette.gradient.map(n => n); // OK -- TS knows gradient is number[]
satisfies neverUse satisfies never in the default case to guarantee all union members are handled. Adding a new member to the union triggers a compile-time error until you handle it.
type Status = 'active' | 'inactive' | 'pending';
function getLabel(status: Status): string {
switch (status) {
case 'active':
return 'Active';
case 'inactive':
return 'Inactive';
case 'pending':
return 'Pending';
default:
// If all cases handled, status narrows to `never`.
// Adding 'archived' to Status errors here until handled.
return status satisfies never;
}
}
When 'archived' is added to Status:
Type 'string' does not satisfy the expected type 'never'.
| Scenario | Use | Why |
|---|---|---|
| Object must match a type, but you need specific key inference | satisfies | Keeps narrow per-key types |
| Variable must be exactly a certain type | : Type annotation | When widening is acceptable or desired |
| Exhaustive switch over a union type | satisfies never in default | Compile-time error on unhandled members |
| Immutable config with known literal values | as const satisfies Type | Combines readonly narrowing with validation |
// Bad: annotation widens, you lose per-key type info
const config: Record<string, string | boolean> = {
apiUrl: 'https://api.example.com',
debug: true,
};
config.debug === true; // Error: can't compare string | boolean reliably
// Good: satisfies validates while preserving narrow types
const config = {
apiUrl: 'https://api.example.com',
debug: true,
} satisfies Record<string, string | boolean>;
config.debug === true; // OK: TS knows debug is boolean
satisfies never when switching on unions// Bad: no exhaustive check -- adding new union members silently falls through
switch (status) {
case 'active': return 'Active';
case 'inactive': return 'Inactive';
// 'pending' is silently unhandled
}
// Good: satisfies never catches missing cases at compile time
switch (status) {
case 'active': return 'Active';
case 'inactive': return 'Inactive';
default: return status satisfies never; // Error: Type '"pending"' does not satisfy 'never'
}