satisfies Operator
Check a value against a type while keeping the narrow inferred type — config objects, route maps, and as const without losing literals.
- typescript
- satisfies-operator
Before satisfies, you chose between two pains:
type Colors = Record<string, string>;
// Annotation: checks shape, widens values to string
const palette: Colors = { primary: '#0f0', danger: '#f00' };
// palette.primary is string, not '#0f0'
// as const: keeps literals, no check against Colors
const palette2 = { primary: '#0f0', danger: '#f00' } as const;
satisfies validates against a type and preserves the inferred (often narrower) type of the expression.
const palette = {
primary: '#0f0',
danger: '#f00',
} as const satisfies Record<string, string>;
// palette.primary is '#0f0'
// typos / wrong value types still error
Docs: satisfies operator (TS 4.9+).
Config objects
type FeatureConfig = {
enabled: boolean;
rollout: number; // 0–100
};
const features = {
darkMode: { enabled: true, rollout: 100 },
newCheckout: { enabled: false, rollout: 10 },
} as const satisfies Record<string, FeatureConfig>;
type FeatureName = keyof typeof features; // 'darkMode' | 'newCheckout'
Add a feature missing rollout → compile error. Keys stay literal.
Route maps
type Route = { path: `/${string}`; auth: boolean };
const routes = {
home: { path: '/', auth: false },
settings: { path: '/settings', auth: true },
} as const satisfies Record<string, Route>;
Note: '/' may need a slightly looser path type depending on template constraints — adjust the Route type to match reality.
Event name maps
const handlers = {
onClick: (e: MouseEvent) => console.log(e.clientX),
onKeyDown: (e: KeyboardEvent) => console.log(e.key),
} satisfies {
[K: string]: (event: Event) => void;
};
Each handler keeps its specific event parameter type under many TS versions when written carefully — validate in your version; the win is catching wrong handler shapes.
vs type annotation
| Approach | Validates | Keeps literals / narrow keys |
|---|---|---|
: Type |
Yes | Often no (widens) |
as Type |
No (forces) | N/A — assertion |
as const |
No | Yes |
satisfies Type |
Yes | Yes |
as const satisfies Type |
Yes | Yes (strongest combo) |
vs assertion
const bad = { primary: 123 } as Record<string, string>; // lies
const good = { primary: 123 } satisfies Record<string, string>; // error
Never use as when satisfies expresses “check me.”
Arrays of unions
type Role = 'admin' | 'user' | 'guest';
const roles = ['admin', 'user'] as const satisfies readonly Role[];
// cannot include 'superadmin'
When not to use it
- Variable you want widened to the annotation type for mutability (
let x: string = 'a'). - Values that truly are the wide type.
- Older TS (< 4.9) — upgrade or use intermediate checks.
Footguns
- Forgetting
as constwhen you need readonly literal keys. satisfieson a widened expression — order matters; apply to the object literal.- Expecting deep immutability — still a type-level tool.
Interview out-loud answer
“satisfies ensures a value matches a type without widening the inferred type. I use it for config and route maps with as const satisfies … so keys stay literal and missing fields fail compile. It’s a check, not a cast.”
Related on this site
- Readonly and const assertions
- Literal types
- Type assertions safely
- keyof typeof and indexed access
- Enums vs union types
Further reading
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.