The TypeScript satisfies Operator and When to Use It
Learn how the satisfies operator validates types without widening, preserving literal inference for config objects and lookup tables.
What you'll learn
- ✓What problem the satisfies operator solves
- ✓How it differs from type annotations and as casts
- ✓Combining satisfies with as const for lookup tables
Prerequisites
- •Basic TypeScript types and interfaces
TypeScript 4.9 introduced the satisfies operator to fill a gap that had bothered developers for years. You could annotate a variable with a type (losing precision) or use as to assert a type (losing safety). The satisfies operator gives you both: it validates an expression against a type while preserving the narrower inferred type.
The Problem: Annotations Widen Your Types
Consider a configuration object that maps feature flags to either a boolean or a string override.
type FlagValue = boolean | string;
type Flags = Record<string, FlagValue>;
const flags: Flags = {
darkMode: true,
apiUrl: 'https://api.example.com',
};
// flags.darkMode is FlagValue, not boolean
// flags.apiUrl is FlagValue, not string
// You cannot use flags.apiUrl.toUpperCase() without narrowing
The annotation Flags is correct, but every value widens to the union boolean | string. You lose the specific type of each key.
How satisfies Fixes This
The satisfies operator checks the expression against the target type but keeps the inferred type intact.
type FlagValue = boolean | string;
const flags = {
darkMode: true,
apiUrl: 'https://api.example.com',
} satisfies Record<string, FlagValue>;
// flags.darkMode is boolean
// flags.apiUrl is string
flags.apiUrl.toUpperCase(); // works without narrowing
TypeScript validates the shape. If you add an invalid value, it reports an error. But the variable retains precise per-key types downstream.
satisfies vs Annotation vs as
These three mechanisms occupy related but distinct roles.
type Config = { mode: 'dev' | 'prod'; port: number };
// Annotation: widens to the declared type
const a: Config = { mode: 'dev', port: 3000 };
// typeof a.mode => 'dev' | 'prod'
// Assertion: trusts the developer, weak check
const b = { mode: 'dev', port: 3000 } as Config;
// typeof b.mode => 'dev' | 'prod'
// satisfies: validates and preserves narrow type
const c = { mode: 'dev', port: 3000 } satisfies Config;
// typeof c.mode => 'dev'
Only c retains the literal 'dev'. If you misspell a property or add an invalid one, satisfies catches it at compile time, just like an annotation would. But unlike an annotation, it does not widen.
Preserving Keys in Record Types
One of the most valuable patterns is using satisfies to keep keyof precise.
const palette = {
primary: '#0f62fe',
danger: '#da1e28',
success: '#24a148',
} satisfies Record<string, string>;
type ColorKey = keyof typeof palette;
// 'primary' | 'danger' | 'success'
function applyColor(key: ColorKey) {
document.body.style.color = palette[key];
}
applyColor('primary'); // valid
applyColor('warning'); // compile error
Without satisfies, keyof typeof palette would just be string. With it, you get a precise union of the actual keys you defined.
Combining With as const
For maximum precision, pair satisfies with as const. The as const freezes every value to its literal type and marks everything readonly, then satisfies validates the shape.
const HTTP_METHODS = {
GET: { method: 'GET', hasBody: false },
POST: { method: 'POST', hasBody: true },
PUT: { method: 'PUT', hasBody: true },
DELETE: { method: 'DELETE', hasBody: false },
} as const satisfies Record<string, { method: string; hasBody: boolean }>;
type Method = keyof typeof HTTP_METHODS;
// 'GET' | 'POST' | 'PUT' | 'DELETE'
// Each entry retains its literal types
// HTTP_METHODS.GET.hasBody is false, not boolean
This is the canonical pattern for type-safe lookup tables in TypeScript.
Discriminated Unions in Arrays
When you build an array of discriminated union members, satisfies validates each element while preserving the specific variant.
type Action =
| { type: 'increment'; amount: number }
| { type: 'reset' }
| { type: 'set'; value: number };
const actions = [
{ type: 'increment', amount: 5 },
{ type: 'reset' },
{ type: 'set', value: 42 },
] satisfies Action[];
// actions[0] is { type: 'increment'; amount: number }
// No narrowing needed to access .amount
console.log(actions[0].amount);
Practical Example: Route Definitions
type RouteConfig = {
path: string;
auth: boolean;
roles?: readonly string[];
};
const routes = {
home: { path: '/', auth: false },
dashboard: { path: '/dashboard', auth: true, roles: ['admin', 'user'] },
settings: { path: '/settings', auth: true, roles: ['admin'] },
} satisfies Record<string, RouteConfig>;
// Precise key union for type-safe route references
type RouteName = keyof typeof routes;
function navigate(route: RouteName) {
window.location.href = routes[route].path;
}
navigate('dashboard'); // valid
navigate('login'); // compile error
When Not to Use satisfies
Use a regular annotation when you want the wider type intentionally. For example, if a variable will be reassigned to different valid values over time, the annotation communicates that intent clearly.
// Annotation is better here because mode will change
let mode: 'dev' | 'prod' = 'dev';
mode = 'prod'; // allowed
Do not use satisfies in function signatures. Parameters and return types should be explicit annotations for readability and documentation.
Also remember that satisfies is a compile-time-only check. For runtime data from APIs or user input, pair it with a runtime validator like Zod or a type guard.
Common Mistakes
Trying to use satisfies on a variable declaration with let and expecting the constraint to hold across reassignments does not work. The check happens once at the expression boundary.
// The satisfies check only applies to the initial value
let config = { port: 3000 } satisfies { port: number };
config = { port: 'abc' }; // Error, but because of inferred type, not satisfies
Another mistake is using satisfies when you actually need a type assertion for an unsafe cast. If the compiler cannot see that the value fits the type, satisfies will correctly reject it. That is a feature, not a bug.
Wrap Up
The satisfies operator validates an expression against a type without widening its inferred type. Use it for config objects, lookup tables, constant arrays, and anywhere you want compile-time validation paired with narrow downstream types. Combine it with as const for the tightest possible inference.
Related articles
- TypeScript TypeScript Generics: Advanced Patterns and Constraints
Master advanced TypeScript generics patterns including conditional constraints, recursive types, generic factories, and type-safe builder patterns with examples.
- TypeScript TypeScript Type Guards and Narrowing Explained
Learn how TypeScript type guards and control flow narrowing work, including typeof, instanceof, in, discriminated unions, and custom type predicates.
- TypeScript TypeScript Type Guards Deep Dive
Master TypeScript type guards: typeof, instanceof, discriminated unions, user-defined predicates, and assertion functions. Narrow types safely and idiomatically.
- TypeScript tsconfig.json Deep Dive: Every Option Explained
A comprehensive guide to tsconfig.json covering compiler options, module resolution, path aliases, project references, and recommended configurations.