The satisfies Operator
Reviewed & published by Brayan K
The satisfies operator validates that a value conforms to a type without widening the value's inferred type, so you get both contract-checking and the precise types you actually wrote.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- What 'value satisfies Type' checks and what it preserves
- Why a ': Type' annotation can widen and lose inference
- Why 'as Type' skips checking entirely
- How satisfies keeps literal types and callable members precise
- Apply satisfies to config objects and route maps
- Choose between ':', 'as', and 'satisfies' confidently
💡 Real-World Analogy
Think of an airport security check. An annotation (:) is like swapping your bag for a standard-issue one that "fits the rules" — it passes, but you've lost your specific belongings (precise types). An assertion (as) is waving a badge to skip the scanner entirely — fast, but nothing was actually checked. satisfies is the proper scan: your own bag goes through, it's verified against the rules, and you keep everything exactly as it was.
1. Check Without Widening
Write value satisfies Type and TypeScript verifies the value matches the type — reporting any mismatch — while leaving the value's inferred type untouched. So a port: 8080 stays a number (not widened to string | number), and number operations on it remain valid. You get the safety of a contract with none of the precision loss.
// 'satisfies' = validate against a type, but KEEP the inferred type.
const config = {
port: 8080,
host: "localhost",
} satisfies Record<string, string | number>;
config.port; // type: number (NOT string | number)
config.port + 1; // ✅ known numeric
config.host.toUpperCase(); // ✅ known string
// Compare with a plain annotation, which WIDENS:
const widened: Record<string, string | number> = { port: 8080, host: "localhost" };
// widened.port is now 'string | number' — you lost the precise inference.// 'satisfies' CHECKS a value against a type WITHOUT changing (widening) the
// value's inferred type. In TS:
// const config = { port: 8080, host: "localhost" } satisfies Record<string, string|number>;
// You still get the SPECIFIC inferred type (port is number), AND the value is
// validated against the constraint. At runtime 'satisfies' adds nothing.
const config = { port: 8080, host: "localhost" };
// Because the value keeps its precise type, number ops on 'port' are safe:
console.log(config.port + 1); // 8081
console.log(config.host.toUpperCase()); // LOCALHOST
// With a plain ': Record<string, string|number>' annotation you would LOSE
// that precision — port would be 'string | number', so 'port + 1' wouldn't
// be known to be numeric. 'satisfies' validates WITHOUT that loss.2. satisfies vs : vs as
Three constructs relate a value to a type, and they behave very differently. A type annotation (:) declares the variable to be that type, often widening literals away. An assertion (as) forces the type with no validation. satisfies sits between them: it validates like an annotation but preserves the precise inferred type like nothing else.
const value = { a: 1, b: 2 };
// 1) ANNOTATION — coerces and can widen; literal types may be lost:
const x: Record<string, number> = value; // x.a is 'number' (fine here)
// 2) ASSERTION — no checking; mistakes pass silently:
const y = value as Record<string, string>; // ❌ wrong, but TS won't warn
// 3) SATISFIES — checks AND preserves the precise inferred type:
const z = value satisfies Record<string, number>; // z.a stays the literal-friendly
// inferred type; bad values error
// Rule: use ':' to declare a variable's type, 'as' only as an escape hatch,
// and 'satisfies' when you want validation WITHOUT losing inference.// THREE ways to relate a value and a type — they behave differently:
// const a: T = value; // ANNOTATION: forces value to T, may WIDEN it
// const b = value as T; // ASSERTION: trusts you, no real check
// const c = value satisfies T; // CHECK: validates AND keeps precise type
// At runtime all three are erased; we show the precision each preserves.
const palette = { primary: "#ff0000", accent: "#00ff00" };
// With 'satisfies Record<string, string>' the keys stay LITERAL ("primary",
// "accent"), so this lookup is known-safe and autocomplete works:
console.log(palette.primary); // #ff0000
console.log(palette.accent); // #00ff00
// 'satisfies' would also CATCH a wrong value type at compile time:
// const bad = { primary: 0xff0000 } satisfies Record<string, string>;
// // ❌ number is not assignable to string (the check still fires)
// Whereas 'as Record<string, string>' would SILENCE that — assertions skip
// the check. 'satisfies' is the safe middle ground.
console.log(Object.keys(palette).join(", ")); // primary, accent3. Real-World: Config Objects & Route Maps
The pattern shines on objects mixing value kinds — a route map with string paths and URL-builder functions, a theme with literal color tokens, a handler dictionary. You want to guarantee every value fits the contract, yet still call the functions and read the literals directly. satisfies gives you that: validated as a whole, precise per property.
// A config that must conform to a shape, but keeps each value's exact type:
const routes = {
home: "/",
user: (id: string) => `/users/${id}`,
} satisfies Record<string, string | ((id: string) => string)>;
routes.home; // type: string (not the whole union)
routes.user("42"); // ✅ callable — kept its function type
// routes.home("x"); // ❌ string isn't callable — caught because type is precise
// Without 'satisfies', a ': Record<...>' annotation would type EVERY value as
// the union 'string | ((id: string) => string)', so you'd have to narrow
// before calling routes.user. 'satisfies' avoids that ceremony entirely.// REAL-WORLD: 'satisfies' shines on config objects and route maps where you
// want BOTH validation AND precise inference. In TS:
// const routes = {
// home: "/",
// user: (id) => `/users/${id}`,
// } satisfies Record<string, string | ((id: string) => string)>;
// Each value keeps its exact type: routes.user is callable, routes.home is a string.
const routes = {
home: "/",
user: (id) => "/users/" + id,
};
// Because inference is preserved, we can CALL the function value directly:
console.log(routes.home); // /
console.log(routes.user("42")); // /users/42
// And string members keep string methods:
console.log(routes.home.length); // 1
// The 'satisfies' constraint guarantees every value matched the shape, while
// each property kept its own specific type — the best of both worlds.🎯 Your Turn
Because satisfies keeps each size a number, you can do math and append a unit. Fill in the blank marked ___ with the CSS unit, then run it.
// 🎯 YOUR TURN — replace the ___ then press "Try it Yourself".
// In TS: const sizes = { sm: 12, md: 16, lg: 24 } satisfies Record<string, number>;
// Because 'satisfies' keeps each value as a number, we can do math and append a unit.
const sizes = { sm: 12, md: 16, lg: 24 };
function px(key) {
return sizes[key] + "___"; // 👉 the CSS unit suffix: "px"
}
console.log(px("sm")); // 12px
console.log(px("lg")); // 24px
// ✅ Expected output:
// 12px
// 24pxCommon Errors (and the fix)
- ❌ Using : Type and then losing literal/number precision — the annotation widened your value. ✅ Switch to satisfies Type to validate without widening.
- ❌ Using as Type to "apply a shape" — no checking, so wrong values slip through. ✅ Use satisfies Type, which actually validates.
- ❌ Expecting satisfies to change a variable's type — it doesn't; it only checks. ✅ If you truly need the variable to BE the type, use a : annotation.
- ❌ "Type ... does not satisfy the expected type ..." — a value genuinely doesn't match. ✅ Fix the offending property; the operator just caught a real bug.
Pro Tips
- 💡 Default to satisfies for literal config — theme tokens, route maps, feature flags — to validate the shape while keeping exact types.
- 💡 Combine with as const (value as const satisfies T) when you want both deep readonly literals and a contract check.
- 💡 Reach for satisfies instead of as almost every time you're shaping a literal — it's strictly safer.
- 💡 Requires TypeScript 4.9+. If satisfies errors as a syntax problem, your compiler is too old.
📚 Keep learning
- TypeScript 4.9 Release Notes — The satisfies Operator
- TypeScript Handbook — Type Assertions (for contrast)
- TypeScript Docs — const assertions
Mini-Challenge: Typed Handler Map
No blanks this time — just a brief and a starting outline. Build the dispatcher yourself, run it, and check your output against the example in the comments.
// 🎯 MINI-CHALLENGE: a typed handler map (a perfect 'satisfies' use case).
// In TS:
// const handlers = {
// click: (x) => "clicked " + x,
// scroll: (y) => "scrolled to " + y,
// } satisfies Record<string, (arg: unknown) => string>;
// Each value stays its precise function type, AND the whole object is validated.
//
// Write dispatch(event, arg) that:
// 1. Looks up handlers[event].
// 2. If found, calls it with 'arg' and returns the result.
// 3. Otherwise returns "no handler".
//
// ✅ Expected output:
// clicked button
// scrolled to 100
// no handler
const handlers = {
click: (x) => "clicked " + x,
scroll: (y) => "scrolled to " + y,
};
// your code here🎉 Lesson Complete
- ✅ satisfies validates a value against a type without widening it
- ✅ A : annotation declares (and can widen); as asserts (no check)
- ✅ satisfies keeps literals and callable members precise
- ✅ Perfect for config objects, route maps, themes, and handler dictionaries
- ✅ Prefer satisfies over as whenever shaping a literal (TS 4.9+)
Practice quiz
What does 'value satisfies Type' do?
- Casts the value to Type
- Converts the value at runtime
- Validates the value against Type without changing its inferred type
- Widens the value to Type
Answer: Validates the value against Type without changing its inferred type. satisfies checks conformance while preserving the precise inferred type.
After 'const c = { port: 8080 } satisfies Record<string, number>', what is the type of c.port?
- number
- string | number
- 8080 widened to any
- unknown
Answer: number. satisfies keeps the precise inference, so c.port stays number.
How does a ': Type' annotation differ from satisfies?
- They are identical
- The annotation skips checking
- The annotation is runtime-only
- The annotation declares the variable to be Type and can widen it
Answer: The annotation declares the variable to be Type and can widen it. An annotation makes the variable that type, often widening literals; satisfies only checks.
How does 'as Type' differ from satisfies?
- as performs validation too
- as asserts with no real check, so wrong values pass silently
- as preserves inference better
- they are the same
Answer: as asserts with no real check, so wrong values pass silently. as is an unchecked assertion; satisfies actually validates the value.
Does satisfies change the value at runtime?
- No, it is erased - compile-time only
- Yes, it coerces
- Yes, it freezes the value
- Only in strict mode
Answer: No, it is erased - compile-time only. Like other type operators, satisfies adds nothing at runtime.
Why is satisfies ideal for a route map with strings and functions?
- It runs the functions
- It widens every value to a union
- Each value keeps its exact type while the whole object is validated
- It deletes the functions
Answer: Each value keeps its exact type while the whole object is validated. routes.home stays a string and routes.user stays callable - precise per property, validated as a whole.
What happens if a property's value violates the satisfies constraint?
- It is silently ignored
- You get a compile-time error
- It throws at runtime
- It is auto-corrected
Answer: You get a compile-time error. satisfies reports a mismatch as a compile error - that's the validation.
Wherever you were about to write 'as SomeShape' on a literal, you should prefer:
- any
- a double assertion
- JSON.stringify
- satisfies SomeShape
Answer: satisfies SomeShape. satisfies checks the value instead of trusting you blindly, so it's strictly safer.
Which combination gives deep readonly literals AND a contract check?
- value as const
- value as const satisfies T
- value satisfies T
- value as T
Answer: value as const satisfies T. as const makes deep readonly literals; satisfies adds the contract validation.
Which TypeScript version introduced the satisfies operator?
- 3.0
- 4.0
- 4.9
- 5.5
Answer: 4.9. satisfies requires TypeScript 4.9 or newer.
Continue this course
- Previous: Recursive Types
- Next: Branded (Nominal) Types — Stop mixing ids and units by giving primitives a unique brand
- Quick reference: TypeScript cheat sheet
Frequently asked questions
What does the satisfies operator do?
It checks that a value conforms to a given type WITHOUT changing the value's inferred type. You write 'value satisfies SomeType'. If the value doesn't match, you get a compile error; if it does, the variable keeps its precise, narrowly-inferred type rather than being widened to SomeType. It's the tool for 'validate this against a contract, but let me keep the exact types I wrote.'
How is satisfies different from a ': Type' annotation?
A type annotation like 'const x: T = value' declares the variable's type to be exactly T, which can WIDEN the value — literal types become their base types and specific members become the annotated union. 'satisfies' instead validates the value against T while leaving the inferred type intact, so a port stays 'number' and a key stays a literal. Use ':' when you want the variable to BE that type; use 'satisfies' when you only want to CHECK it.
How is satisfies different from 'as'?
'as' is a type assertion: it tells the compiler to trust you, performing no real validation, so a wrong value passes silently and can crash at runtime. 'satisfies' actually checks the value against the type and reports mismatches as errors, while also preserving precise inference. Reach for 'satisfies' whenever you were tempted to use 'as' to 'apply a shape' to a literal — it's the safe alternative.
When should I reach for satisfies in real code?
Configuration objects, route tables, theme/palette maps, and handler dictionaries are the sweet spot: you want to guarantee the object matches a contract (every value is the right kind) while still getting exact types for each property (this one is callable, that one is a literal string). 'satisfies' gives you both validation and inference in one expression, which a plain annotation or assertion can't.