typescript-satisfies-operator
TypeScript: The satisfies Operator
Core Concept
The satisfies operator validates that an expression matches a type without changing the inferred type. This is different from type annotations (:) which widen the type.
Key insight from Matt Pocock:
- "When you use a colon, the type BEATS the value"
- "When you use
satisfies, the value BEATS the type"
Type Annotation vs Satisfies
type RoutingPathname = "/products" | "/cart" | "/checkout";
// Type annotation - widens to union
const url1: RoutingPathname = "/products";
// url1 is typed as: RoutingPathname (wide)
// Satisfies - keeps literal
const url2 = "/products" satisfies RoutingPathname;
// url2 is typed as: '/products' (narrow)
// Why it matters:
const test1: "/products" = url1; // Error: RoutingPathname not assignable to '/products'
const test2: "/products" = url2; // Works
Classic Use Case: Object Validation with Preserved Types
type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
// Type annotation loses specific property types
const palette1: Record<Colors, string | RGB> = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
};
palette1.green.toUpperCase(); // Error: 'toUpperCase' doesn't exist on string | RGB
// Satisfies validates AND preserves literal types
const palette2 = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255], // Error: Typo caught!
} satisfies Record<Colors, string | RGB>;
palette2.green.toUpperCase(); // Works - green is inferred as string
When to Use What
| Annotation Style | Type vs Value | Use Case |
|---|---|---|
: Type (colon) |
Type wins | Need wider type for reassignment |
satisfies Type |
Value wins | Need validation + narrow inference |
as Type |
Lies to TS | Escape hatch (use sparingly!) |
| No annotation | Inference | Most common - let TS infer |
Rule of Thumb
Use satisfies when:
- You want the EXACT type of the variable, not the wider type
- The type is complex enough that you want validation you didn't mess it up
Use colon annotation when:
- You need to reassign the variable later with different values of the union
- You explicitly want the wider type
Common Pattern: as const satisfies
Combine as const for immutability with satisfies for validation:
const routes = {
home: "/",
products: "/products",
cart: "/cart",
} as const satisfies Record<string, string>;
// routes.home is typed as '/' (readonly literal)
// But validated against Record<string, string>
Prefer as const satisfies Over Type Annotation
When you need both validation AND literal type preservation:
// Bad - type annotation widens types, loses literals
const LANG_MAP: Record<string, string> = {
en: '1',
cs: '2',
} as const;
// LANG_MAP.en is just string, not '1'
// Good - satisfies validates while preserving literal types
const LANG_MAP = {
en: '1',
cs: '2',
} as const satisfies Record<string, string>;
// LANG_MAP.en is '1' (narrow literal type)
Real-World Example: Config Validation
type Locale = 'en' | 'cs';
// Validates all locales are present, preserves specific values
const SHOP_GRAPHQL_LOCALE_LANGUAGE_ID_MAP = {
en: '1',
cs: '2',
} as const satisfies Record<Locale, string>;
// TypeScript will error if you miss a locale:
const INCOMPLETE_MAP = {
en: '1',
// cs: '2', // Error: Property 'cs' is missing
} as const satisfies Record<Locale, string>;
Real-World Examples
Configuration Objects
type Config = {
api: string;
timeout: number;
retries: number;
};
// Validates shape, but keeps literal types for autocomplete
const config = {
api: "https://api.example.com",
timeout: 5000,
retries: 3,
} satisfies Config;
// config.api is 'https://api.example.com', not string
Event Handlers Map
type EventMap = Record<string, (...args: unknown[]) => void>;
const handlers = {
click: (x: number, y: number) => console.log(x, y),
submit: (data: FormData) => console.log(data),
} satisfies EventMap;
// handlers.click is (x: number, y: number) => void
// Not (...args: unknown[]) => void
Exhaustive Checks with Records
type Status = "pending" | "approved" | "rejected";
const statusLabels = {
pending: "Waiting for review",
approved: "Approved",
rejected: "Rejected",
} satisfies Record<Status, string>;
// If you add a new Status, TypeScript will error until you add it here
References
More from flpbalada/fb-skills
progressive-disclosure
Reduce complexity by revealing information progressively. Use when designing
6discuss-task
Clarify ambiguous tasks before action. Use when goal, scope, success criteria, constraints, or risks are unclear.
4cognitive-fluency-psychology
Apply cognitive fluency principles to improve clarity, trust, and conversion.
4react-useeffect-avoid
Guides when NOT to use useEffect and suggests better alternatives. Use when reviewing React code, troubleshooting performance, or considering useEffect for derived state or form resets.
4discuss-code
Critically discuss code issues with compact findings. Use when code needs review for logic, simplicity, structure, naming, or maintainability.
4learn
Extract reusable patterns from the current session. Use when errors, debugging techniques, workarounds, or project conventions should become skills.
3