Suggest an editImprove this articleRefine the answer for “The satisfies operator in TypeScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`satisfies`** - validates that a value matches a type at compile time while keeping the inferred narrow type. Unlike `: Type`, it does not widen. ```typescript const colors = { red: "#ff0000", green: [0, 255, 0] } satisfies Colors; colors.red.toUpperCase(); // ✅ string, not string | number[] ``` **Key:** validates without widening. Zero runtime cost.Shown above the full answer for quick recall.Answer (EN)Image**`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 - `: Type` assigns the broad type to your variable and loses details; `satisfies Type` validates against the type but keeps the narrow inference - Analogy: type annotation reshapes your value to fit the declared type; `satisfies` checks the shape without reshaping it - Use `satisfies` when 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 ```typescript 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 const` is too narrow (readonly literals) and you want flexible validation: use `satisfies` - 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 `satisfies` over 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** ```typescript // 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** ```typescript // 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** ```typescript // 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 narrowed ``` If 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 for `og:title` processing - **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 ```typescript 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); // works ``` With `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 ```typescript 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` ```typescript 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.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.