TypeScript satisfies Operator Cheat Sheet
Validate types without widening them -- satisfies vs annotations, satisfies vs as, config patterns, and when not to use it.
The Problem: Type Widening
Type annotations widen values to the annotated type, losing specific literal information. The `satisfies` operator checks the type without widening.
// With type annotation: TypeScript widens to Record<string, string>
const routes: Record<string, string> = {
home: '/',
about: '/about',
blog: '/blog',
};
routes.home; // type: string (not '/')
routes.oops; // type: string -- no error, even though 'oops' doesn't exist!// With satisfies: TypeScript validates AND preserves the exact type
const routes = {
home: '/',
about: '/about',
blog: '/blog',
} satisfies Record<string, string>;
routes.home; // type: '/' (literal preserved!)
// routes.oops; // Error: Property 'oops' does not exist
// // TypeScript knows the exact keystype ColorMap = Record<string, [number, number, number]>;
// Annotation: loses key information
const a: ColorMap = { red: [255, 0, 0], green: [0, 128, 0] };
a.red; // type: [number, number, number]
a.purple; // type: [number, number, number] -- no error
// satisfies: validates shape, keeps exact keys and values
const b = { red: [255, 0, 0], green: [0, 128, 0] } satisfies ColorMap;
b.red; // type: [number, number, number]
// b.purple; // Error: Property 'purple' does not existThis is the core tradeoff: type annotations give you the annotated type, satisfies gives you the inferred type after validation.
satisfies vs Type Annotation
A type annotation says "treat this as type T." The satisfies operator says "verify this matches T, but keep the inferred type."
type Theme = {
primary: string;
secondary: string;
};
// Annotation widens values to string
const theme: Theme = {
primary: '#3b82f6',
secondary: '#10b981',
};
theme.primary; // type: string (not '#3b82f6')type Theme = {
primary: string;
secondary: string;
};
// satisfies validates but preserves literals
const theme = {
primary: '#3b82f6',
secondary: '#10b981',
} satisfies Theme;
theme.primary; // type: '#3b82f6' (literal preserved)type Config = { host: string; port: number };
// Annotation: catches extra properties
// const a: Config = { host: 'localhost', port: 3000, debug: true };
// Error: 'debug' does not exist in type 'Config'
// satisfies: also catches extra properties
// const b = { host: 'localhost', port: 3000, debug: true } satisfies Config;
// Error: 'debug' does not exist in type 'Config'
// Both catch excess properties. The difference is what type you get back.Use annotation when you want consumers to see the abstract type. Use satisfies when you want consumers to see the specific value.
satisfies vs as
Type assertions (`as`) override the compiler. `satisfies` works with the compiler. One is a safety net, the other is a bypass.
type Status = 'active' | 'inactive';
// as lets you lie to the compiler
const status = 'invalid' as Status; // No error!
// status is type Status, but the value is 'invalid' at runtime
// as bypasses type checking -- use only when you know more than TStype Status = 'active' | 'inactive';
// satisfies catches the mistake
// const status = 'invalid' satisfies Status;
// Error: '"invalid"' does not satisfy 'Status'
const status = 'active' satisfies Status; // OK
// status is type 'active' (narrower than Status)type User = { name: string; age: number };
const raw = JSON.parse('{"name":"Alice","age":30}');
// as: compiles, but raw could be anything at runtime
const userA = raw as User;
// satisfies doesn't help here -- raw is typed as 'any'
// You need runtime validation (Zod, Valibot, etc.) for JSON parsing
// The right tool for JSON: runtime validation, not type-level operatorsNeither `as` nor `satisfies` validates data at runtime. For user input, API responses, or parsed JSON, use a runtime validation library.
Preserving Literal Types
The biggest win for `satisfies`: keeping string, number, and boolean literals exact while still validating the overall shape.
type Level = 'debug' | 'info' | 'warn' | 'error';
type LogConfig = { level: Level; prefix: string };
// Annotation widens level to Level
const logA: LogConfig = { level: 'warn', prefix: '[APP]' };
logA.level; // type: Level (could be any of the 4)
// satisfies preserves 'warn' as a literal
const logB = { level: 'warn', prefix: '[APP]' } satisfies LogConfig;
logB.level; // type: 'warn' (exact)type RGB = [number, number, number];
type Palette = Record<string, RGB>;
const palette = {
ocean: [0, 119, 190],
sunset: [255, 99, 71],
} satisfies Palette;
// Each value is inferred as a readonly tuple, not number[]
palette.ocean; // type: [number, number, number]
palette.sunset; // type: [number, number, number]type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
// Annotation: TypeScript only knows it's some Shape
const shapeA: Shape = { kind: 'circle', radius: 5 };
// shapeA.radius; // Error: 'radius' doesn't exist on Shape
// satisfies: TypeScript knows it's specifically a circle
const shapeB = { kind: 'circle', radius: 5 } satisfies Shape;
shapeB.radius; // OK: type number (TS knows it's the circle variant)Config Object Pattern
The most common use of `satisfies` in practice: validating configuration objects while keeping autocomplete on exact keys and values.
type RouteConfig = {
path: string;
auth: boolean;
roles?: string[];
};
const routes = {
dashboard: { path: '/dashboard', auth: true, roles: ['admin', 'user'] },
login: { path: '/login', auth: false },
settings: { path: '/settings', auth: true, roles: ['admin'] },
} satisfies Record<string, RouteConfig>;
// Autocomplete works for route keys
routes.dashboard.path; // type: '/dashboard' (literal)
routes.dashboard.roles; // type: string[] (known to exist)
// Typos caught at compile time:
// routes.dashbord; // Error: did you mean 'dashboard'?type FeatureFlag = {
enabled: boolean;
rolloutPercent?: number;
description: string;
};
const flags = {
darkMode: { enabled: true, description: 'Dark theme toggle' },
newCheckout: { enabled: false, rolloutPercent: 25, description: 'Redesigned checkout flow' },
betaDashboard: { enabled: true, rolloutPercent: 100, description: 'New analytics dashboard' },
} satisfies Record<string, FeatureFlag>;
// Type-safe access with autocomplete on flag names
if (flags.darkMode.enabled) {
// TypeScript knows darkMode.enabled is true (literal)
}type Translations = Record<string, string>;
const en = {
greeting: 'Hello',
farewell: 'Goodbye',
error_not_found: 'Page not found',
} satisfies Translations;
const fr = {
greeting: 'Bonjour',
farewell: 'Au revoir',
error_not_found: 'Page non trouvee',
} satisfies Translations;
// Both validated as Translations, but you still get
// autocomplete on exact keys like en.greetingsatisfies with Record
Record types are where `satisfies` shines brightest. Annotations erase your keys; `satisfies` keeps them.
// Annotation: any string key is valid
const colors: Record<string, string> = {
red: '#ff0000',
green: '#00ff00',
blue: '#0000ff',
};
colors.red; // OK: type string
colors.banana; // OK: type string (no error -- bad!)// satisfies: only declared keys are valid
const colors = {
red: '#ff0000',
green: '#00ff00',
blue: '#0000ff',
} satisfies Record<string, string>;
colors.red; // OK: type '#ff0000'
// colors.banana; // Error: 'banana' does not existtype Theme = 'light' | 'dark' | 'system';
// Validate all required keys exist
const themeLabels = {
light: 'Light Mode',
dark: 'Dark Mode',
system: 'System Default',
} satisfies Record<Theme, string>;
// If you forget one:
// const incomplete = {
// light: 'Light Mode',
// dark: 'Dark Mode',
// } satisfies Record<Theme, string>;
// Error: Property 'system' is missingWith a finite key union like `Theme`, `satisfies Record<Theme, string>` ensures every key is present. This is a compile-time exhaustiveness check for object keys.
When NOT to Use satisfies
satisfies is not always the right choice. Here are situations where a type annotation or `as const` works better.
type Config = { host: string; port: number };
// If a function returns Config, annotation is better:
function getConfig(): Config {
return { host: 'localhost', port: 3000 };
}
// Callers see Config, not { host: 'localhost'; port: 3000 }
// This keeps the public API stable when implementation changes
// satisfies on a return value doesn't affect the return type:
function getConfig2() {
return { host: 'localhost', port: 3000 } satisfies Config;
}
// Return type is { host: string; port: number } -- same shape,
// but now callers depend on the implementation details// If you just want literals without validation:
const directions = ['north', 'south', 'east', 'west'] as const;
// type: readonly ['north', 'south', 'east', 'west']
// satisfies adds nothing here unless you also need shape validation:
const directions2 = ['north', 'south', 'east', 'west'] as const
satisfies readonly string[];
// Same literals, but validated as string array// Don't add satisfies just because you can:
const name: string = 'Alice'; // fine -- 'Alice' literal not useful here
const count: number = 42; // fine -- 42 literal not useful here
// satisfies shines when the annotation would erase USEFUL specificity.
// If you don't need the literal type downstream, an annotation is simpler.Reach for `satisfies` when you need both validation and literal preservation. If you only need one of those, a type annotation or `as const` is simpler and communicates intent more clearly.