Validate object literals precisely without losing their literal types
Introduced in TypeScript 4.9, the satisfies operator closes a gap between explicit type annotations and as type assertions. It checks whether an expression matches a type while preserving the expression's own, narrower type.
Table of Contents
- 1. The Problem: Annotation or Assertion
- 2. Syntax Basics
- 3. Combining satisfies With as const
- 4. Using satisfies With Record and Union Keys
- 5. Configuration Objects and Function Return Values
- 6. Nested Structures and Arrays
- 7. Comparison to as and Explicit Annotations
- 8. Best Practices for Real Projects
- 9. Common Pitfalls
- 10. Summary
- 11. FAQ
1. The Problem: Annotation or Assertion
Before TypeScript 4.9, checking an object literal against a type meant choosing between two tools, and both came with drawbacks. An explicit type annotation such as const config: Config = { ... } reliably validates the structure, but it widens every property to the declared type. A field typed as string stays string, even if the actual value can only ever be "json" or "xml".
The alternative was a type assertion using as. It preserves the narrower literal types, but it completely skips structural validation by the compiler. Typos in property names or missing required fields only surface at runtime, if at all.
The satisfies operator combines both properties: it validates the expression against a type and reports errors for missing or incorrect fields, while still preserving the expression's own inferred type instead of widening it to the checked type.
2. Syntax Basics
The syntax is deliberately simple: an expression is followed by the satisfies keyword and a type. The compiler checks compatibility, but the resulting type of the variable remains the original, narrower type of the expression.
In the example below, a configuration object is checked against an interface. Despite the check, the type of route.method stays the literal "GET" rather than the wider string type.
This order, expression first and type second, deliberately sets satisfies apart from a type annotation, where the type comes before the expression. The compiler reads the operator as a check applied after the fact, not as a declaration fixed up front, which is exactly what affects how inference behaves.
interface RouteConfig {
method: string;
path: string;
cache: boolean;
}
const route = {
method: "GET",
path: "/products",
cache: true,
} satisfies RouteConfig;
// route.method has the type "GET", not string
function handle(method: "GET" | "POST") {
// ...
}
handle(route.method); // works because "GET" stays a literal
3. Combining satisfies With as const
satisfies becomes especially useful together with as const. While as const fixes every property to its narrowest, immutable literal type, satisfies adds structural validation against an expected interface on top.
Without satisfies, a plain as const declaration leaves you on your own to make sure the shape actually matches the expected schema. With satisfies you get both at once: validation and maximum type precision.
type Theme = {
primary: string;
secondary: string;
radius: number;
};
const theme = {
primary: "#0f172a",
secondary: "#e11d48",
radius: 8,
} as const satisfies Theme;
// theme.radius has the type 8, not number
// theme.primary has the type "#0f172a", not string
4. Using satisfies With Record and Union Keys
A common use case is validating objects with a fixed set of keys, such as a map of feature flags or routes. With a Record type as the target, satisfies ensures every expected key is present without generalizing the concrete value types.
This pays off especially when you keep working with the individual values afterward, for instance in a switch statement that narrows on concrete literals.
type Feature = "checkout" | "wishlist" | "reviews";
const featureFlags = {
checkout: true,
wishlist: false,
reviews: true,
} satisfies Record<Feature, boolean>;
// A missing or extra key is reported by the compiler immediately.
5. Configuration Objects and Function Return Values
In applications with many configuration objects, such as route definitions, API client options, or theme settings, satisfies pays off in a real way. Instead of giving every object its own type annotation that discards literal information, full precision is preserved while typos still get caught.
For functions that return configuration objects, satisfies can also be applied directly to the return expression, so callers benefit from the narrower types too.
interface ApiOptions {
baseUrl: string;
timeout: number;
retries: number;
}
function buildOptions() {
return {
baseUrl: "https://mironsoft.de/api",
timeout: 5000,
retries: 3,
} satisfies ApiOptions;
}
const options = buildOptions();
// options.timeout has the type 5000
6. Nested Structures and Arrays
satisfies isn't limited to flat objects, it also works with nested structures and arrays of objects. The compiler recursively checks every level of the structure against the given type while preserving the concrete literal types at each level.
That enables, for example, arrays of navigation entries where each entry still keeps its exact path string as a type, which adds extra type safety when combined with routing libraries.
interface NavItem {
label: string;
href: string;
external?: boolean;
}
const navigation = [
{ label: "Magento", href: "/magento" },
{ label: "Hyva", href: "/hyva" },
{ label: "Blog", href: "https://mironsoft.de/blog", external: true },
] satisfies NavItem[];
7. Comparison to as and Explicit Annotations
The key difference from as lies in error checking. An as assertion essentially tells the compiler to trust you without performing a real structural check. If a required property is missing, as reports no error, while satisfies does.
The difference from an explicit type annotation lies in the resulting type. An annotation such as const x: Config = {...} widens the properties to the types declared in the interface. satisfies, on the other hand, still infers the type from the expression itself and only uses the given interface for validation.
In practice this means: choosing as trades away safety for control, while choosing a type annotation trades away precision for simplicity. satisfies asks for neither trade-off, which is exactly why it should be the default choice in most situations.
8. Best Practices for Real Projects
satisfies is a great fit for configuration objects, lookup tables, theme definitions, and anything where the exact literal values are used later on. For plain variables whose literal types are never used again, a normal annotation often remains sufficient.
In library code consumed by many callers, satisfies helps keep public constants as precisely typed as possible without redundantly redeclaring the internal structure.
9. Common Pitfalls
A frequent misunderstanding is that satisfies changes the declared type of the variable. That is not the case: the resulting type remains the inferred type of the expression, not the checked type. Anyone who explicitly wants the checked type as the variable's type still needs an additional annotation.
A second point concerns the minimum version: satisfies requires TypeScript 4.9 or newer. In older projects still running an earlier version, the syntax causes a parser error. Editor autocompletion also benefits from the feature: while typing inside the object literal, the editor already suggests the target type's fields, even though the resulting type stays narrower.
A third pitfall involves build pipelines using older transpilers that understand the rest of the TypeScript code but aren't yet built against the current language version. Upgrading the toolchain is worth checking before introducing satisfies.
| Aspect | Type Annotation | as Assertion | satisfies |
|---|---|---|---|
| Structural check | Yes | No | Yes |
| Literal types preserved | No | Yes | Yes |
| Missing required property | Error | No error | Error |
| Resulting variable type | Declared type | Asserted type | Inferred type of the expression |
| Minimum version | All versions | All versions | TypeScript 4.9+ |
Mironsoft
TypeScript migration, type safety, and team onboarding
A JavaScript codebase without type safety, but no time for a full migration?
We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.
Migration Roadmap
Plan and execute a gradual JS-to-TS migration without big-bang risk.
Strict Mode Rollout
Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.
Team Onboarding
Bring developers up to speed on TypeScript best practices with workshops and reviews.
10. Summary
Satisfies Operator
Available since
TypeScript 4.9
Runtime overhead
None, pure compiler feature
Combines well with
as const, Record, arrays
Typical use
Configuration objects, lookup tables