The satisfies Operator: Validate a Value Without Widening It
A type annotation checks a value but throws away what it knows about it. `satisfies` checks against a type while keeping the narrow, literal type the value actually has.
For years TypeScript forced an awkward trade-off on every literal you wrote down: either annotate it — and lose the precise type the literal had — or leave it un-annotated and get no validation at all. The satisfies operator (TypeScript 4.9) removes the trade-off. It checks a value against a type without changing the value's inferred type. Once you see the pattern you'll reach for it constantly.
1. The problem: annotation widens
Say you have a palette and you want to be sure every entry is a valid color. The obvious move is an annotation:
type Color = string | [number, number, number];
const palette: Record<string, Color> = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
};
// You know red is a tuple... but the type doesn't.
palette.red.at(0); // ok-ish
palette.red.toUpperCase(); // NO ERROR — Color includes string
The annotation did its job as a check, but it also widened every value to Color. TypeScript no longer remembers that red is specifically a tuple and green is specifically a string, so it can't catch palette.red.toUpperCase(). You validated the object and lost all the precision in the same line.
2. The other half of the trade-off: no annotation
Drop the annotation and inference keeps the precise types — but now nothing checks that the values are actually valid colors:
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
};
// palette.red is number[], palette.green is string — precise!
// ...but a typo like 'reed: [999, 0, 0]' sails through unchecked.
So the old choice was: validation OR precision, pick one.
3. What satisfies does
satisfies gives you both. It asserts that the expression is assignable to a type, raising a compile error if not, but the inferred type of the expression is left untouched:
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Record<string, Color>;
palette.red.at(0); // ok — red is still [number, number, number]
palette.red.toUpperCase(); // ERROR — tuples don't have toUpperCase
palette.green.toUpperCase(); // ok — green is still string
The crisp way to remember it: an annotation flows the type down onto the value; satisfies flows the value's type up to be checked, then steps out of the way. You get the error on an invalid entry and you keep the narrow per-key types.
4. The most common win: typed config with key checking
The pattern shines on configuration objects where you want the keys constrained but the values kept exact:
type Route = { path: string; method: "GET" | "POST" };
const routes = {
list: { path: "/users", method: "GET" },
create: { path: "/users", method: "POST" },
remove: { path: "/users/:id", method: "DELETE" }, // ERROR: "DELETE" not allowed
} satisfies Record<string, Route>;
// And because the keys weren't widened to 'string':
routes.list; // ok, autocompletes
routes.typo; // ERROR: property 'typo' does not exist
An annotation of Record<string, Route> would have made routes.typo legal (any string key is allowed on a Record<string, ...>). With satisfies, the object's key set stays literal — "list" | "create" | "remove" — so a misspelled access is a compile error and you get autocomplete on the real keys.
5. satisfies plays well with as const
as const and satisfies solve different problems and compose. as const makes everything deeply readonly and maximally narrow; satisfies validates the shape. Use both when you want a frozen, precisely-typed lookup that is still type-checked:
const httpStatus = {
ok: 200,
notFound: 404,
teapot: 418,
} as const satisfies Record<string, number>;
type Status = typeof httpStatus[keyof typeof httpStatus]; // 200 | 404 | 418
Order matters: write as const satisfies T. as const narrows first, then satisfies checks the narrowed value against T without re-widening it. The result is a readonly object whose values are the literal numbers, and you've proven every value is a number along the way.
6. Where it does NOT help
satisfies is a check, not a cast. Two things to keep straight:
- It never changes runtime behavior. Like every type-level construct it is erased during compilation — there is zero output. It only affects what the checker infers and rejects.
- It cannot launder an invalid value the way
ascan.value as Ttells the compiler "trust me" and suppresses errors;value satisfies Tdoes the opposite — it adds a constraint and will error if the value doesn't genuinely conform. If you find yourself wantingsatisfiesto silence an error, you actually wanted to fix the value (or you wantedas, which is a different, riskier tool).
Also note it checks assignability, not exhaustiveness: { a: 1 } satisfies Record<string, number> is fine even if you meant to list more keys. If you need "every key of a union must be present," encode that in the target type (e.g. Record<MyUnion, T>) so a missing key becomes an error.
7. A quick decision guide
- You want a variable to be a wider type for later reassignment → use an annotation (
let x: Color = ...). - You want to validate a literal but keep its exact inferred type → use
satisfies. - You want a deeply-frozen, literal-typed constant that is also validated →
as const satisfies T. - You want to override the checker because you know something it doesn't → use
as(sparingly, and know you're turning the safety off).
Rules of thumb
- Annotation widens,
satisfiesdoesn't. Usesatisfieswhenever you want validation and the precise per-value type — config objects, palettes, lookup tables, route maps. - It keeps key sets literal.
satisfies Record<string, T>still catches typo'd property access and gives autocomplete, which aRecordannotation throws away. - Compose with
as constasas const satisfies Tfor frozen, literal-typed, validated constants. - It's a constraint, not a cast. If
satisfieserrors, the value is genuinely wrong — fix the value; don't reach forasto silence it. - Zero runtime cost. It's erased at compile time like every other type annotation.