The TypeScript satisfies Operator: Real Patterns

Photo by Martin Vorel via Wikimedia Commons (CC BY-SA 4.0)
It checks that a value conforms to a given type without changing the value's own inferred type. Unlike a type annotation, which widens the value to the declared type, satisfies keeps the narrow literal type TypeScript inferred from what you wrote. You still get errors for typos, missing keys, or wrong value types.
An as assertion silences the compiler and forces a type, so it can hide real errors like typos or missing properties. The satisfies operator does the opposite: it validates the value against a type and reports mismatches, while preserving the value's specific inferred type. Use as only when you genuinely know more than the compiler.
Use them together when you need both exact literal, readonly values and schema validation. Write the value, then as const to lock the literals, then satisfies to check the shape. The order matters: putting satisfies after as const ensures the readonly literal type is what gets validated against your constraint.
The satisfies operator was introduced in TypeScript 4.9, released in November 2022. It has been stable ever since, so any project on TypeScript 5.x or the Go-native 7.0 compiler released in 2026 already supports it. There is no runtime cost because it is a pure type-level construct that erases at compile time.
Yes. Using satisfies against a Record keyed by a union type gives you a compile-time completeness check. If you forget a key the build fails, and if you add an unknown key it also fails. This makes it ideal for feature flags, permission maps, and locale dictionaries where every case must be handled.

Photo by Martin Vorel via Wikimedia Commons (CC BY-SA 4.0)
Key Takeaway
The satisfies operator checks that a value conforms to a type without widening the value to that type. You keep the narrow, literal type TypeScript inferred from what you actually wrote, while still catching typos, missing keys, and wrong value types at author time. It sits between a type annotation and an as cast.
For years I wrote config objects in TypeScript and quietly accepted a bad trade. If I annotated the object with a type, I got validation but lost the specific values the compiler knew about. If I skipped the annotation, I kept the specific values but got zero checking. The satisfies operator, added in TypeScript 4.9 back in late 2022 and now something I reach for weekly, dissolves that trade. This post is the set of patterns I actually use it for in production NestJS and Next.js code.
To understand why satisfies exists, look at the two tools it replaces. A type annotation forces the value up to match the declared type, so the compiler forgets the specific keys and literals you wrote. A type assertion with as does the opposite: it silences the compiler entirely, so a typo or a missing property sails right through. Neither gives you validation plus precision at the same time.
type RouteConfig = Record<string, { path: string; auth: boolean }>;
// Annotation: validated, but the value is WIDENED.
const routesA: RouteConfig = {
home: { path: "/", auth: false },
admin: { path: "/admin", auth: true },
};
routesA.dashboard; // no error — TS forgot the exact keys
// Cast: keeps keys, but silences real errors.
const routesB = {
home: { path: "/", atuh: false }, // typo — NOT caught
} as RouteConfig;With satisfies, the value keeps its own inferred type and the declared type is used only as a constraint to check against. The mental model I use: with an annotation the type wins, with satisfies the value wins. You get an error if the object violates the constraint, but the variable's type stays exactly as narrow as what you wrote. That means autocomplete on the real keys and no phantom properties.
const routes = {
home: { path: "/", auth: false },
admin: { path: "/admin", auth: true },
} satisfies Record<string, { path: string; auth: boolean }>;
routes.home.path; // string, autocompleted
routes.dashboard; // Error: property 'dashboard' does not exist
// and a typo like `atuh: false` fails right here at author timeThe clearest one-line rule I give teammates: use an annotation when you want the variable reusable as the wide type, use satisfies when you want to lock in the exact shape you wrote and still be told when it drifts. Reach for as only when you genuinely know more than the compiler.
This is the canonical example from the TypeScript 4.9 release notes and the one that sold me. Say a config holds values that are sometimes strings and sometimes tuples. Annotate it and every value collapses to the union, so you can no longer call string methods on the string entries without narrowing. With satisfies the constraint is enforced, but each property keeps its precise type and its methods stay available.
type Color = string | [number, number, number];
const palette = {
primary: "#0ea5e9",
danger: [239, 68, 68],
muted: "#64748b",
} satisfies Record<string, Color>;
palette.primary.toUpperCase(); // OK — TS knows it's a string
palette.danger.map((c) => c); // OK — TS knows it's a tuple
// Without satisfies (plain annotation), both lines would error.When keys must cover a known union exactly, satisfies against a Record keyed by that union gives you a compile-time completeness check. Forget a key and the build fails; add an unknown key and it fails too. I use this for feature flags, permission maps, and locale dictionaries, so a new enum member cannot be shipped half-wired.
type Role = "admin" | "editor" | "viewer";
const permissions = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
} satisfies Record<Role, string[]>;
// If you add a fourth Role later and forget it here,
// this line stops compiling — a free exhaustiveness guard.
function can(role: Role): string[] {
return permissions[role];
}On its own, as const freezes a value to its deepest literal, readonly type but gives you no schema validation, so a typo in a key or a wrong-typed value goes unnoticed while you write it. Combine the two and you get both: the literals are locked and the shape is checked. The order matters. Put satisfies after as const so the readonly literal type is what gets validated against the constraint.
const endpoints = {
users: "/api/users",
orders: "/api/orders",
} as const satisfies Record<string, `/api/${string}`>;
type Endpoint = typeof endpoints[keyof typeof endpoints];
// Endpoint = "/api/users" | "/api/orders" — exact literals,
// AND every value is verified to match the /api/ template.Do not flip the order to satisfies before as const. That validates the widened, mutable object first and then freezes it, which can lose the literal narrowing you wanted. When you need both, the sequence is always the value, then as const, then satisfies.
It is not a universal replacement for annotations. If a value is a function parameter or a class field that other code assigns to as the wide type, annotate it, because you want the wide type to be the contract. Use satisfies for terminal values you author once and read many times: configs, maps, constant tables. And never use it to paper over an error you do not understand, that is what as tempts you to do and satisfies deliberately does not.
| Approach | Validates shape | Keeps narrow type | Best for |
|---|---|---|---|
| Annotation (colon type) | Yes | No — widens | Reusable variables, params, fields |
| as cast | No — silences errors | Yes | Rare escapes when you know more than TS |
| satisfies | Yes | Yes | Config objects, route/permission maps |
| as const + satisfies | Yes | Yes — literal + readonly | Constant tables needing exact literals |
None of this requires a bleeding-edge toolchain. The operator has been stable since TypeScript 4.9, so any project on 5.x or the new Go-native 7.0 compiler that shipped mid-2026 already has it. There is no runtime cost either, satisfies is a pure type-level construct that erases completely on compile, exactly like an annotation. It is one of the highest-leverage, lowest-risk habits I have picked up in years of daily TypeScript.