TypeScript 85 🔷 Satisfies Operator
The satisfies operator, introduced in TypeScript 4.9, solves a long-standing dilemma: you want to validate that a value conforms to a type, but you don’t want to lose the specific type that TypeScript would infer on its own. Before satisfies, you had two imperfect choices. You could use a type annotation (: SomeType), which validates the value but widens its type to the annotation, losing literal types and autocomplete precision . Or you could use a type assertion (as SomeType), which preserves inference in some cases but bypasses type checking entirely, allowing values that don’t actually conform .
The satisfies operator does neither of those things. It checks that the expression on the left matches the type on the right, and if the check passes, the expression keeps its inferred type — the narrowest possible type — rather than being widened to the target type .
Key point: satisfies is a compile-time-only operator. It generates no JavaScript, no runtime code, and no performance cost. It is purely a type-checking tool . The value on the left must conform to the type on the right. If it does, TypeScript keeps the narrower inferred type for the variable. If it does not, you get a compiler error at the satisfies clause.
Why the satisfies operator exists
TypeScript developers face a recurring dilemma when declaring objects and arrays.
The annotation problem. When you annotate a variable with a type, TypeScript treats that annotation as the variable’s type. It does not infer a narrower type. If you write const config: Record<string, string | number> = { apiUrl: 'https://api.example.com', timeout: 5000 }, then config.apiUrl is string | number, not the literal type 'https://api.example.com' . You lose the precision that would allow TypeScript to catch typos like config.pinkValue when the object only has red, green, and blue keys .
The inference problem. If you omit the annotation entirely, const config = { apiUrl: 'https://api.example.com', timeout: 5000 }, TypeScript infers the narrow type. But then there is no validation — if you accidentally add a property with the wrong type, or miss a required property, the compiler says nothing .
The assertion problem. The as keyword tells TypeScript to trust you. It performs no structural check. const config = { apiUrl: 123 } as Record<string, string> compiles without error, even though apiUrl is a number, not a string. The error surfaces at runtime .
The satisfies solution. satisfies gives you validation and inference. The value is checked against the type. If it conforms, the variable keeps its narrow inferred type. If it does not, the compiler reports an error at the satisfies clause .
The trade-off. satisfies does not change the variable’s type. If you need the variable to be exactly the declared type — for reassignment to other values of that type — you still need an annotation. satisfies is for the common case where the initial value is the only value, and you want both validation and precision.
a. satisfies vs Type Annotations vs as
The three ways to assign a type to a value behave differently.
A colon annotation (const x: T = value) declares that the variable is of type T. The value must be assignable to T. The variable’s type is T, no matter how specific the value is .
A type assertion (const x = value as T) tells the compiler to treat value as type T. No structural check is performed. If value is not actually a T, the compiler does not care. The variable’s type is T .
The satisfies operator (const x = value satisfies T) checks that value conforms to T. The variable’s type is the inferred type of value — the narrowest possible type. If value does not conform, the compiler errors .
type RGB = [number, number, number];
// Annotation — validates, but widens
const paletteAnnotated: Record<string, RGB> = {
red: [255, 0, 0],
green: "#00ff00", // ❌ Error: string not assignable to RGB
bleu: [0, 0, 255],
};
// as — no validation
const paletteAsserted = {
red: [255, 0, 0],
green: "#00ff00", // no error — as bypasses checking
bleu: [0, 0, 255],
} as Record<string, RGB>;
// satisfies — validates, preserves inference
const palette = {
red: [255, 0, 0],
green: "#00ff00", // ❌ Error caught here
bleu: [0, 0, 255],
} satisfies Record<string, RGB>;
The satisfies version catches the error that as misses, and it preserves the exact inferred type that the annotation widens. In the corrected version, palette.red is inferred as [255, 0, 0] — a tuple of literal numbers — not just RGB .
The rule of thumb: use satisfies when you want validation without widening. Use an annotation when the variable should be exactly the declared type. Use as only when you have independent evidence the type is correct and the compiler cannot see it .
b. The Core Use Cases: Configuration and Const Objects
The satisfies operator is most commonly used with configuration objects and constant collections.
Configuration objects. A Next.js getServerSideProps function must conform to a specific signature. Before satisfies, you either annotated the function, which forced you to specify the generic Props type explicitly, or you left it unannotated and lost type checking on the context parameter .
// Before satisfies — verbose or unsafe
export const getServerSideProps: GetServerSideProps<MyPageData> = async (context) => {
return { props: { locale: context.locale, rating: Math.random() } };
};
// After satisfies — concise and safe
export const getServerSideProps = (async (context) => {
return { props: { locale: context.locale, rating: Math.random() } };
}) satisfies GetServerSideProps<MyPageData>;
The satisfies version validates the function signature without forcing the generic type to be written on the function itself.
Const objects with literal keys. A route map or color palette should have exact keys. With an annotation, TypeScript allows access to any key that matches the index signature. With satisfies, only the actual keys are valid .
interface RouteConfig {
[path: string]: { method: "GET" | "POST"; auth: boolean };
}
// Annotation — routes.anything is allowed
const routesAnnotated: RouteConfig = {
"/users": { method: "GET", auth: true },
"/login": { method: "POST", auth: false },
};
routesAnnotated.typo; // no error
// satisfies — routes.anything is an error
const routes = {
"/users": { method: "GET", auth: true },
"/login": { method: "POST", auth: false },
} satisfies RouteConfig;
routes.typo; // ✅ Error: Property 'typo' does not exist
routes["/users"].method; // type: "GET"
The satisfies version preserves the literal keys and values, so routes["/users"].method is "GET", not the wider "GET" | "POST" .
c. Combining with as const
The as const assertion and satisfies serve complementary purposes. as const makes an object deeply readonly and preserves literal types. satisfies validates the object against a type without changing its type.
When used together, as const must come first .
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
} as const satisfies {
apiUrl: string;
timeout: number;
retries: number;
};
config.apiUrl; // type: "https://api.example.com" (literal)
config.timeout = 3000; // ❌ Error: Cannot assign to 'timeout' (readonly)
Without as const, the properties are mutable and the types are narrower than string and number only if TypeScript infers them that way — but they remain writable. With as const, the properties are readonly and the literal types are preserved. The satisfies clause then validates the shape .
The order matters. { ... } satisfies T as const is a type error because as const can only apply to a literal expression, not to the result of a satisfies check .
Complete Example Session
This session builds a validated color palette, a route configuration, and a Next.js-style data fetching function.
// ============================================
// PART 1: THE ANNOTATION TRAP
// ============================================
type RGB = [red: number, green: number, blue: number];
type Color = RGB | string;
const paletteAnnotated: Record<string, Color> = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255],
};
// paletteAnnotated.green is Color, not "#00ff00"
// paletteAnnotated.typo is allowed (index signature)
// ============================================
// PART 2: THE SATISFIES FIX
// ============================================
const palette = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255],
} satisfies Record<string, Color>;
// palette.green is string (narrower than Color)
// palette.red is [255, 0, 0] (literal tuple)
// palette.typo; // ❌ Error: Property 'typo' does not exist
// ============================================
// PART 3: THE ERROR CATCH
// ============================================
// ❌ satisfies catches the wrong type
const badPalette = {
red: [255, 0, 0],
green: 123, // ❌ Error: number is not assignable to Color
bleu: [0, 0, 255],
} satisfies Record<string, Color>;
// ============================================
// PART 4: THE ROUTE CONFIGURATION
// ============================================
interface Route {
path: string;
component: () => string;
auth?: boolean;
}
const routes = [
{ path: "/", component: () => "Home" },
{ path: "/users", component: () => "Users" },
{ path: "/admin/users", component: () => "AdminUsers", auth: true },
] satisfies Route[];
// routes[0].path is "/", not string
type RoutePath = (typeof routes)[number]["path"]; // "/" | "/users" | "/admin/users"
// ============================================
// PART 5: THE NEXT.JS PATTERN
// ============================================
interface GetServerSideProps<Props> {
(context: { query: Record<string, string> }): Promise<{ props: Props }>;
}
interface MyPageData {
locale: string | undefined;
rating: number;
}
// Before satisfies — generic must be explicit
export const getServerSidePropsOld: GetServerSideProps<MyPageData> = async (context) => ({
props: { locale: context.query.locale, rating: Math.random() },
});
// After satisfies — concise, validated
export const getServerSideProps = (async (context) => ({
props: { locale: context.query.locale, rating: Math.random() },
})) satisfies GetServerSideProps<MyPageData>;
// ============================================
// PART 6: THE as const COMBINATION
// ============================================
const settings = {
mode: "dark",
fontSize: 14,
shortcuts: ["ctrl+s", "ctrl+z"],
} as const satisfies {
mode: string;
fontSize: number;
shortcuts: readonly string[];
};
// settings.mode is "dark" (literal), readonly
// settings.fontSize is 14 (literal), readonly
// settings.shortcuts is readonly ["ctrl+s", "ctrl+z"]
// ============================================
// PART 7: THE SATISFIES VS ANNOTATION CHOICE
// ============================================
// Use satisfies when you want the initial value's exact type
const explicitValue = { status: "active" } satisfies { status: string };
// explicitValue.status is "active"
// Use annotation when the variable should be the declared type
let mutableValue: { status: string } = { status: "active" };
mutableValue = { status: "inactive" }; // ✅ allowed
// ============================================
// PART 8: THE UTILITY TYPE COMBINATION
// ============================================
type User = {
username: string;
email: string;
firstName: string;
lastName: string;
};
const minimalUser = {
username: "joe",
email: "joe@example.com",
} satisfies Pick<User, "username" | "email">;
const noEmail = {
username: "joe",
firstName: "Joe",
lastName: "Hiyden",
} satisfies Omit<User, "email">;
// ============================================
// PART 9: THE ARRAY SATISFIES
// ============================================
const colors = [
{ name: "red", hex: "#ff0000" },
{ name: "green", hex: "#00ff00" },
] satisfies Array<{ name: string; hex: string }>;
// colors[0].name is "red", not string
type ColorName = (typeof colors)[number]["name"]; // "red" | "green"
// ============================================
// PART 10: THE COMPILE-TIME ONLY NATURE
// ============================================
// satisfies generates no JavaScript output.
// The emitted code is identical to the code without satisfies.
// It is purely a type-checking construct.
const value = { x: 1 } satisfies { x: number };
// Compiles to: const value = { x: 1 };
The ten parts cover the annotation trap, the satisfies fix, the error catch, route configuration, the Next.js pattern, the as const combination, the choice between satisfies and annotation, utility type combinations, array satisfies, and the compile-time-only nature.
Quick Reference
The Three Ways to Assign Types
| Syntax | Validates | Preserves Inference | Variable Type |
|---|---|---|---|
: T | Yes | No | T |
as T | No | Sometimes | T |
satisfies T | Yes | Yes | Inferred |
The satisfies Rules
| Rule | Description |
|---|---|
| Introduced in | TypeScript 4.9 |
| Compile-time only | No runtime output |
| Value must conform | Errors at satisfies clause |
| Type is inferred | Narrowest possible |
as const order | Must come before satisfies |
The Use Cases
| Use Case | Why satisfies |
|---|---|
| Config objects | Validate shape, keep literal keys |
| Route maps | Exact path literals |
| Color palettes | Exact tuple and literal types |
| Next.js data fetching | Validate function signature |
| Const collections | Preserve literal values |
The satisfies vs Annotation
| Scenario | Use |
|---|---|
| Single value, want precision | satisfies |
| Variable may be reassigned | : T |
| Need exact literal types | satisfies |
| Need readonly + validation | as const satisfies |
Best Practices
✅ Do This:
// Use satisfies for config objects
const config = { apiUrl: "https://...", timeout: 5000 } satisfies Config; // ✅
// Use satisfies to validate while keeping literal types
const palette = { red: [255, 0, 0] } satisfies Record<string, RGB>; // ✅
// Combine as const and satisfies for readonly + validation
const settings = { mode: "dark" } as const satisfies Settings; // ✅
// Use satisfies to catch typos in route keys
const routes = { "/users": {} } satisfies Record<string, {}>; // ✅
routes.typo; // Error
// Use satisfies for function signature validation
const handler = (async (ctx) => ({ props: {} })) satisfies GetServerSideProps<Props>; // ✅
❌ Don’t Do This:
// Don't use annotation when you want the literal type
const palette: Record<string, RGB> = { red: [255, 0, 0] }; // ❌ widens
// Don't use as for validation
const config = { wrong: 123 } as Config; // ❌ no error
// Don't put satisfies before as const
const x = { a: 1 } satisfies T as const; // ❌ type error
// Don't use satisfies when you need reassignment
let x = { mode: "dark" } satisfies Settings; // ❌ x is readonly
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Typo not caught | Used annotation, not satisfies | Use satisfies Record<string, T> |
| Lost literal type | Used : T annotation | Use satisfies T |
| No error on wrong value | Used as T | Use satisfies T |
as const satisfies error | Wrong order | as const must come first |
| Variable type too narrow | Used satisfies when reassignment needed | Use annotation instead |
isolatedDeclarations issue | as const satisfies requires annotation | Add explicit type or use two-line workaround |
Real-World Examples
1. Configuration Object
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
} satisfies { apiUrl: string; timeout: number };
2. Color Palette
const palette = {
red: [255, 0, 0],
green: "#00ff00",
} satisfies Record<string, string | [number, number, number]>;
3. Route Map
const routes = {
"/": "Home",
"/users": "Users",
} satisfies Record<string, string>;
4. Readonly Config
const settings = {
mode: "dark",
fontSize: 14,
} as const satisfies { mode: string; fontSize: number };
5. Next.js Data Fetching
const getServerSideProps = (async (ctx) => ({
props: { locale: ctx.query.locale },
})) satisfies GetServerSideProps<MyPageData>;
6. Array of Objects
const users = [
{ id: "1", role: "admin" },
] satisfies Array<{ id: string; role: string }>;
7. Pick Utility
const minimal = { username: "joe", email: "joe@x.com" } satisfies Pick<User, "username" | "email">;
8. Omit Utility
const noEmail = { username: "joe", firstName: "Joe" } satisfies Omit<User, "email">;
9. Function Signature
const handler = ((x: number) => x * 2) satisfies (x: number) => number;
10. Exact Literal Keys
const theme = { primary: "#000", secondary: "#fff" } satisfies Record<string, string>;
type ThemeKey = keyof typeof theme; // "primary" | "secondary"
Visual: satisfies vs Annotation vs as
┌──────────────────────────────────────────────┐
│ ANNOTATION (: T) │
│ │
│ const x: T = { literal: "value" }; │
│ │
│ ✅ Validates │
│ ❌ Widens type to T │
│ ❌ x.literal is T["literal"], not "value" │
│ │
├──────────────────────────────────────────────┤
│ ASSERTION (as T) │
│ │
│ const x = { literal: "value" } as T; │
│ │
│ ❌ No validation │
│ ✅ Keeps inferred type (sometimes) │
│ ❌ Wrong values slip through │
│ │
├──────────────────────────────────────────────┤
│ SATISFIES (satisfies T) │
│ │
│ const x = { literal: "value" } satisfies T; │
│ │
│ ✅ Validates │
│ ✅ Keeps inferred type │
│ ✅ x.literal is "value" │
│ │
└──────────────────────────────────────────────┘
Visual: The Config Object Use Case
┌──────────────────────────────────────────────┐
│ WITHOUT satisfies │
│ │
│ const config: Record<string, string|number> =│
│ { apiUrl: "https://...", timeout: 5000 }; │
│ │
│ config.apiUrl → string | number │
│ config.typo → allowed (no error) │
│ │
├──────────────────────────────────────────────┤
│ WITH satisfies │
│ │
│ const config = { │
│ apiUrl: "https://...", │
│ timeout: 5000, │
│ } satisfies Record<string, string|number>; │
│ │
│ config.apiUrl → string │
│ config.typo → ❌ Error │
│ │
└──────────────────────────────────────────────┘
Visual: as const satisfies Order
┌──────────────────────────────────────────────┐
│ CORRECT ORDER │
│ │
│ const config = { ... } │
│ as const satisfies Config; │
│ │
│ as const → readonly + literal types │
│ satisfies → validates shape │
│ Result: readonly literal, validated │
│ │
├──────────────────────────────────────────────┤
│ WRONG ORDER │
│ │
│ const config = { ... } │
│ satisfies Config as const; │
│ │
│ ❌ Type error: as const applies only to │
│ literal expressions │
│ │
└──────────────────────────────────────────────┘
Visual: satisfies is Compile-Time Only
┌──────────────────────────────────────────────┐
│ SATISFIES GENERATES NO RUNTIME CODE │
│ │
│ TypeScript source: │
│ const x = { a: 1 } satisfies { a: number }; │
│ │
│ JavaScript output: │
│ const x = { a: 1 }; │
│ │
│ The satisfies clause disappears. │
│ It exists only for the compiler. │
│ Zero runtime overhead. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Introduced | TypeScript 4.9 |
| Purpose | Validate without widening |
| Syntax | value satisfies Type |
| vs Annotation | Validates both, satisfies preserves inference |
vs as | as skips validation, satisfies performs it |
With as const | as const must come first |
| Runtime output | None — compile-time only |
| Use case | Config objects, route maps, const collections |
Key takeaways:
satisfiesvalidates a value against a type without widening its inferred type. The value must conform to the type, but the variable keeps its narrowest possible type — literal values, exact keys, tuple shapes .- It replaces the annotation-vs-inference dilemma. Annotations validate but widen. Inference narrows but does not validate.
satisfiesdoes both . - It is different from
as.asbypasses type checking entirely.satisfiesperforms a full structural check and errors if the value does not conform . as const satisfiescombines readonly and validation.as constpreserves literal types and makes properties readonly.satisfiesvalidates the shape. The order isas constfirst .- It is compile-time only. No JavaScript is generated. No runtime cost. It is purely a type-checking tool .
- Use it for configuration objects and constant collections. These are the cases where you want exact keys, literal values, and validation that the shape is correct .
- Do not use it when the variable needs to be reassigned.
satisfiesdoes not change the variable’s type. If you need a variable of typeTthat can hold otherTvalues, use an annotation.
Remember: The satisfies operator is the answer to a specific question: “How do I check that this value conforms to a type without losing the precise type TypeScript would infer on its own?” Before 4.9, the answer was “you cannot.” After 4.9, you write value satisfies Type and get both validation and inference. It is not a replacement for annotations — annotations are for when the variable should be exactly the declared type. It is not a replacement for as — as is for when you know the type and the compiler does not. It is a third tool for the common case: a value that must conform, and whose exact type you want to keep.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!