The satisfies operator in TypeScript
satisfies - checks if a value matches a type at compile time while keeping the inferred narrow type intact. TypeScript 4.9 added it to fix one specific annoyance: : Type annotation widens your value to the broad type, erasing the specific string, array shape, or literal that TypeScript already knows about.
Theory
TL;DR
: Typeassigns the broad type to your variable and loses details;satisfies Typevalidates against the type but keeps the narrow inference- Analogy: type annotation reshapes your value to fit the declared type;
satisfieschecks the shape without reshaping it - Use
satisfieswhen you need validation AND still want to call.toUpperCase()on a string or.map()on a specific array afterward - Zero runtime cost - fully erased in JS output
- Decision rule: object with mixed value types that you need to access specifically after validation? Use
satisfies
Quick example
type Colors = Record<string, string | number[]>;
// Type annotation: widens to string | number[], loses specifics
const colors: Colors = { red: "#ff0000", green: [0, 255, 0] };
colors.red.toUpperCase(); // ❌ string | number[] has no toUpperCase
// satisfies: validates, then keeps narrow inference
const colors2 = { red: "#ff0000", green: [0, 255, 0] } satisfies Colors;
colors2.red.toUpperCase(); // ✅ TypeScript infers string
colors2.green.map(x => x * 2); // ✅ TypeScript infers number[]The type check runs, catches any mismatch, and then TypeScript uses the narrow inferred type for everything that follows.
Key difference
: Type assigns Type to the variable. From that point, TypeScript sees only string | number[] and forgets that red was specifically a string. satisfies runs the same check but substitutes the original narrow inference back into the declaration. Same compile-time safety, different result downstream.
When to use
- Object needs validation, but you access specific properties after: use
satisfies as constis too narrow (readonly literals) and you want flexible validation: usesatisfies- Config objects, theme maps, route tables, status code constants: all good fits
- Simple primitives (
5 satisfies number): skip it, inference already handles this - Object grows beyond 5 properties and splitting into individually typed fields gets messy: use
satisfiesover annotations
Comparison table
| Approach | Validates? | Preserves narrow type? | Notes |
|---|---|---|---|
const x: Type = value | Yes | No (widens to Type) | Standard annotation |
const x = value as Type | No | No | Skips all checks |
const x = { ... } as const | No | Yes (readonly) | Too narrow for most cases |
const x = { ... } satisfies Type | Yes | Yes | Validation + inference |
| When to use | Computed values, return types | Simple readonly literals | Complex validated objects |
How the compiler handles this
TypeScript's type checker assigns the inferred type of the value to a temporary node, verifies it's assignable to the target type, then puts the original narrow inference back into the declaration. No runtime code is generated. It relies on structural subtyping, the same mechanism as implements on a class. The emitted JavaScript is identical to what you'd get without satisfies.
Common mistakes
Expecting satisfies to fill in missing properties
// Wrong: satisfies checks exact match, not intersection filling
const x = {} satisfies { a: number } & { b: string };
// ❌ Error: missing a and b
// Fix: provide all required properties
const x = { a: 1, b: "hi" } satisfies { a: number } & { b: string }; // ✅Using it on primitives
// Pointless - TypeScript already infers 5 as number
const n = 5 satisfies number;This adds no value and confuses anyone reading the code.
Expecting body narrowing on functions
// satisfies only checks the function signature, not the body
const fn = ((x: string) => x.length) satisfies (x: string) => number;
// The return type is checked, but closure variables inside aren't narrowedIf you need the function itself to be narrowed, use as const.
Nested objects losing inference depth
satisfies checks the top-level structure. Nested objects are validated but not always narrowed as deeply as you'd expect. One edge case: { children: [] } satisfies Tree infers children as never[] because an empty array has no element type to infer from. Apply satisfies closer to where you consume the value, or use a recursive type.
Real-world usage
- Next.js:
const metadata = { title: "App" } satisfies Metadata- validates static exports, keeps the literal string forog:titleprocessing - TanStack Query:
const defaultOptions = { queries: { retry: 3 } } satisfies DefaultOptions- preserves the number literal for override comparisons downstream - tRPC: validating router shapes while keeping procedure-level type inference intact
- Zod:
const config = { ... } satisfies z.infer<typeof schema>- shape check with literal defaults preserved
I've found this pattern most useful in config files with more than five properties. At that point, splitting into individually typed fields gets unwieldy, and as const forces readonly everywhere you don't want it.
Follow-up questions
Q: What's the difference between satisfies, as const, and type assertion?
A: satisfies validates and keeps narrow types. as const keeps narrow types without validation. Type assertion (as Type) skips validation entirely - it's a cast, not a check.
Q: Does satisfies have any runtime impact?
A: None. It's compile-time only, completely erased in the emitted JavaScript. The output is identical to a plain object literal.
Q: Can you combine satisfies with as const?
A: Yes: { ... } as const satisfies Type. This gives you readonly literal types plus compile-time validation against the target type. Order matters - as const first, then satisfies.
Q: How would you polyfill satisfies in a TypeScript 4.8 project?
A: Use a helper type: type Satisfies<T, U extends T> = U. Then write const x = { ... } as Satisfies<Colors, typeof x>. It's awkward but covers most cases before upgrading to 4.9+.
Q: Why not always use satisfies instead of annotations?
A: Annotations work for computed values, function return types, and cases where you actually want the broad type for downstream consumers. satisfies requires a literal value to infer from - it can't help you type a value that doesn't exist yet at the declaration site.
Examples
Basic: colors palette config
type Colors = Record<string, string | number[]>;
const palette = {
red: "#ff0000",
green: [0, 255, 0],
blue: "#0000ff",
} satisfies Colors;
// ✅ TypeScript knows red and blue are strings
palette.red.startsWith("#"); // works
palette.blue.toUpperCase(); // works
// ✅ TypeScript knows green is number[]
palette.green.map(channel => channel * 0.5); // worksWith Colors as the annotation instead, all three properties become string | number[] and none of those method calls compile without a type guard first.
Intermediate: route configuration
type Route = {
path: string;
method: "GET" | "POST" | "PUT" | "DELETE";
handler: string;
};
const routes = {
getUsers: {
path: "/api/users",
method: "GET",
handler: "UserController.getAll",
},
createUser: {
path: "/api/users",
method: "POST",
handler: "UserController.create",
},
} satisfies Record<string, Route>;
// TypeScript knows the exact keys
type RouteKeys = keyof typeof routes; // "getUsers" | "createUser"
// And the exact method literals
routes.getUsers.method; // "GET", not "GET" | "POST" | "PUT" | "DELETE"
// Validation still fires on typos
const bad = {
home: { path: "/", method: "PATCH", handler: "HomeController" },
} satisfies Record<string, Route>;
// ❌ Error: "PATCH" is not assignable to "GET" | "POST" | "PUT" | "DELETE"This pattern shows up in Express middleware setups and Next.js App Router configs. You get the type safety from the annotation without losing the literal types you need for routing logic or switch statements.
Advanced: combining with as const
const palette = {
red: "#ff0000",
green: "#00ff00",
blue: "#0000ff",
} as const satisfies Record<string, `#${string}`>;
// Values are readonly literal types AND validated as hex color strings
type PaletteKey = keyof typeof palette; // "red" | "green" | "blue"
palette.red; // "#ff0000" (readonly literal, not just string)
// Validation catches format violations at compile time
const badPalette = {
yellow: "ffff00", // Missing #
} as const satisfies Record<string, `#${string}`>;
// ❌ Error: "ffff00" is not assignable to `#${string}`as const makes the values readonly literals. satisfies checks they match the target format. Together they give you a validated, fully typed constant map with zero runtime overhead.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.