| |

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

SyntaxValidatesPreserves InferenceVariable Type
: TYesNoT
as TNoSometimesT
satisfies TYesYesInferred

The satisfies Rules

RuleDescription
Introduced inTypeScript 4.9
Compile-time onlyNo runtime output
Value must conformErrors at satisfies clause
Type is inferredNarrowest possible
as const orderMust come before satisfies

The Use Cases

Use CaseWhy satisfies
Config objectsValidate shape, keep literal keys
Route mapsExact path literals
Color palettesExact tuple and literal types
Next.js data fetchingValidate function signature
Const collectionsPreserve literal values

The satisfies vs Annotation

ScenarioUse
Single value, want precisionsatisfies
Variable may be reassigned: T
Need exact literal typessatisfies
Need readonly + validationas 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

PitfallWhy It HappensFix
Typo not caughtUsed annotation, not satisfiesUse satisfies Record<string, T>
Lost literal typeUsed : T annotationUse satisfies T
No error on wrong valueUsed as TUse satisfies T
as const satisfies errorWrong orderas const must come first
Variable type too narrowUsed satisfies when reassignment neededUse annotation instead
isolatedDeclarations issueas const satisfies requires annotationAdd 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

ItemValue
IntroducedTypeScript 4.9
PurposeValidate without widening
Syntaxvalue satisfies Type
vs AnnotationValidates both, satisfies preserves inference
vs asas skips validation, satisfies performs it
With as constas const must come first
Runtime outputNone — compile-time only
Use caseConfig objects, route maps, const collections

Key takeaways:

  • satisfies validates 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. satisfies does both .
  • It is different from as. as bypasses type checking entirely. satisfies performs a full structural check and errors if the value does not conform .
  • as const satisfies combines readonly and validation. as const preserves literal types and makes properties readonly. satisfies validates the shape. The order is as const first .
  • 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. satisfies does not change the variable’s type. If you need a variable of type T that can hold other T values, 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!