The satisfies Operator
Introduced in TypeScript 4.9, the satisfies operator lets you validate that a value conforms to a type without changing the inferred type of the variable. It is the best of both worlds: compile-time shape checking with full literal-type precision.
The Problem Before satisfies
Before satisfies, you had two unsatisfying options when you wanted type safety on an object literal:
type Palette = Record<string, string | [number, number, number]>;
// Option 1: Annotate — validates the shape, but loses literal types
const palette: Palette = {
red: [255, 0, 0],
blue: "#0000ff",
};
// palette.blue is now 'string | [number, number, number]'
// We can't call .toUpperCase() directly — TypeScript doesn't know it's a string
// Option 2: No annotation — keeps literal types, but no shape validation
const palette2 = {
red: [255, 0, 0],
blue: "#0000ff",
};
// palette2.blue is 'string' — but there is no shape contract
// A typo like 'rede' instead of 'red' would go undetectedThe satisfies Solution
satisfies checks that the value matches the type without changing the inferred type. The expression type remains exactly what TypeScript would normally infer.
type Palette = Record<string, string | [number, number, number]>;
const palette = {
red: [255, 0, 0],
blue: "#0000ff",
} satisfies Palette;
// TypeScript validates the shape: ✓ each value is string or [n,n,n]
// TypeScript keeps the precise types:
// palette.red → [number, number, number] (not string | [n,n,n])
// palette.blue → string (not string | [n,n,n])
// Now these work:
const [r, g, b] = palette.red; // array destructuring on red
const upper = palette.blue.toUpperCase(); // string method on bluesatisfies, you get contract validation AND retain the full precision of literal/inferred types — no trade-off.satisfies vs. Type Annotation
Feature | Type annotation (:) | satisfies |
|---|---|---|
Validates shape | Yes | Yes |
Keeps inferred/literal types | No — widens to declared type | Yes — preserves inferred type |
Allows extra properties | No (excess property check) | No (excess property check) |
Can be used mid-expression | No | Yes |
Changes the variable type | Yes | No |
type Config = { host: string; port: number };
// Annotation: variable type becomes Config
const c1: Config = { host: "localhost", port: 3000 };
// c1.host is 'string' (not "localhost")
// c1.port is 'number' (not 3000)
// satisfies: variable type stays precise
const c2 = { host: "localhost", port: 3000 } satisfies Config;
// c2.host is 'string' (same here — "localhost" is widened by let/const anyway)
// c2.port is 'number'
// More powerful with literal types
type Mode = "development" | "production";
type BuildConfig = { mode: Mode; sourceMap: boolean };
const config = {
mode: "development", // inferred as "development" literal
sourceMap: true,
} satisfies BuildConfig;
type InferredMode = typeof config.mode; // "development" (not Mode)satisfies with as const
Combining as const and satisfies is extremely powerful: as const makes types narrowest-possible, and satisfies validates the shape at the same time.
type Routes = Record<string, { path: string; auth: boolean }>;
const ROUTES = {
home: { path: "/", auth: false },
profile: { path: "/profile", auth: true },
admin: { path: "/admin", auth: true },
} as const satisfies Routes;
// Each path is a literal type (not just string)
type HomePath = typeof ROUTES.home.path; // "/"
// TypeScript validates: each value has path: string and auth: boolean ✓
// ROUTES.missing = {} // Error — can't add properties to constas const satisfies Type (const before satisfies). This first freezes the types to literals, then validates against the schema.Validating Configuration Objects
satisfies shines for configuration objects where each key has a specific type but you want per-key type precision downstream.
type LogLevel = "debug" | "info" | "warn" | "error";
type AppConfig = {
port: number;
logLevel: LogLevel;
features: Record<string, boolean>;
database: { url: string; poolSize: number };
};
const appConfig = {
port: 8080,
logLevel: "info",
features: {
darkMode: true,
betaSearch: false,
},
database: {
url: "postgres://localhost/mydb",
poolSize: 10,
},
} satisfies AppConfig;
// Precise types preserved:
appConfig.port; // number (8080 widens to number in let/const)
appConfig.logLevel; // "info" (literal)
appConfig.features.darkMode; // boolean
appConfig.database.url; // string
// This would error (validation):
// const bad = { port: "8080" } satisfies AppConfig; // Error: string not assignable to numbersatisfies in Discriminated Unions
type Widget =
| { kind: "button"; label: string; disabled?: boolean }
| { kind: "input"; placeholder: string; type: "text" | "email" }
| { kind: "checkbox"; checked: boolean; label: string };
// Validate each widget has the right shape, preserve literal kind
const widgets = [
{ kind: "button", label: "Submit", disabled: false },
{ kind: "input", placeholder: "Email", type: "email" },
{ kind: "checkbox", checked: false, label: "Agree" },
] satisfies Widget[];
// Each element has precise types:
const first = widgets[0];
// first.kind is "button" (literal), not the union
// first.label is string, first.disabled is booleansatisfies with Function Return Values
interface Handler {
path: string;
method: "GET" | "POST" | "PUT" | "DELETE";
handler: (req: Request) => Response;
}
// Validate the array of handlers at definition time
const routes = [
{
path: "/api/users",
method: "GET",
handler: (req: Request) => new Response("OK"),
},
{
path: "/api/users",
method: "POST",
handler: (req: Request) => new Response("Created", { status: 201 }),
},
] satisfies Handler[];
// routes[0].method is "GET" (literal), not "GET" | "POST" | "PUT" | "DELETE"Error Catching with satisfies
The key benefit of satisfies is catching shape errors at the point of definition, not at use:
type Color = { r: number; g: number; b: number };
type Theme = Record<"primary" | "secondary" | "accent", Color>;
// Error caught immediately — 'accent' key is missing
const myTheme = {
primary: { r: 255, g: 0, b: 0 },
secondary: { r: 0, g: 255, b: 0 },
// accent is missing — satisfies catches this
} satisfies Theme;
// ^^^^^^^^^^^^^^ Error: Property 'accent' is missing
// Typo in property name
const colors = {
pirmary: { r: 0, g: 0, b: 255 }, // typo: 'pirmary' not 'primary'
} satisfies Partial<Theme>;
// Error: Object literal may only specify known properties, 'pirmary' doesn't existsatisfies (or a type annotation), both of the above errors would silently compile and only appear as runtime bugs.satisfies vs. Type Annotation vs. as const — Cheat Sheet
type Theme = { primary: string; secondary: string };
// 1. Type annotation — validates shape, widens types
const t1: Theme = { primary: "#f00", secondary: "#00f" };
// t1.primary: string (not "#f00")
// 2. as const — narrows to literals, no validation
const t2 = { primary: "#f00", secondary: "#00f" } as const;
// t2.primary: "#f00" — but no check that it matches Theme
// 3. satisfies — validates AND keeps narrow types
const t3 = { primary: "#f00", secondary: "#00f" } satisfies Theme;
// t3.primary: string (string literal widens because const doesn't freeze it)
// 4. as const satisfies — narrows AND validates ← usually what you want
const t4 = { primary: "#f00", secondary: "#00f" } as const satisfies Theme;
// t4.primary: "#f00" — literal AND validated against Themeas const satisfies YourType. It gives you the strictest type safety in both directions: the shape is validated against the contract, and the inferred type retains full literal precision.Design Tokens and Theme Systems
satisfies is especially valuable in design systems where you want to enforce that all theme tokens are present and correctly typed, while retaining literal types for downstream code generation.
type SpacingScale = Record<"xs" | "sm" | "md" | "lg" | "xl", number>;
type ColorPalette = Record<string, string>;
const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
} as const satisfies SpacingScale;
// All five keys required; values remain literal numbers (4, 8, 16, ...)
type ColorToken = "brand" | "accent" | "neutral" | "error" | "success";
type Colors = Record<ColorToken, string>;
const colors = {
brand: "#3b82f6",
accent: "#8b5cf6",
neutral: "#6b7280",
error: "#ef4444",
success: "#22c55e",
} as const satisfies Colors;
// Derive the CSS variable keys at type level
type ColorVar = `--color-${keyof typeof colors}`;
// "--color-brand" | "--color-accent" | "--color-neutral" | ...satisfies with Readonly Arrays
satisfies works on arrays too — validating that every element matches the schema while preserving tuple-level or element-level types.
type Env = "development" | "staging" | "production";
type EnvConfig = { name: Env; url: string; debug: boolean };
const environments = [
{ name: "development", url: "http://localhost:3000", debug: true },
{ name: "staging", url: "https://staging.example.com", debug: true },
{ name: "production", url: "https://example.com", debug: false },
] satisfies EnvConfig[];
// Each element keeps its literal types:
const devName = environments[0].name; // "development" (not Env)
// Combining with as const:
const envs = [
{ name: "development", url: "http://localhost:3000", debug: true },
{ name: "production", url: "https://example.com", debug: false },
] as const satisfies EnvConfig[];
type ProdUrl = typeof envs[1]["url"]; // "https://example.com" (literal)satisfies with Interface Hierarchies
satisfies also works when the validating type is an interface or a complex structural type — not just a Record. This is useful when you have a base contract with optional extension points.
interface Plugin {
name: string;
version: string;
install(app: unknown): void;
uninstall?(): void;
}
// Validates that each plugin matches the Plugin interface
// while keeping the literal 'name' and 'version' types
const routerPlugin = {
name: "router",
version: "1.2.0",
install(app: unknown) { console.log("Router installed"); },
} satisfies Plugin;
const storePlugin = {
name: "store",
version: "2.0.1",
install(app: unknown) { console.log("Store installed"); },
uninstall() { console.log("Store uninstalled"); },
} satisfies Plugin;
// routerPlugin.name is "router" (literal), not string
type RouterName = typeof routerPlugin.name; // "router"Common satisfies Patterns Summary
Pattern | Example | Benefit |
|---|---|---|
Config object | config satisfies AppConfig | Validates all required fields exist |
as const satisfies | { mode: "dark" } as const satisfies Theme | Literal types + shape validation |
Route/handler map | routes satisfies Handler[] | Exhaustive route type checking |
Design tokens | colors as const satisfies Colors | Token completeness + literal values |
Discriminated union array | actions satisfies Action[] | Per-action field checking |
Plugin registry | plugin satisfies Plugin | Interface compliance + literal name |
satisfies in Library Code
When writing a library or framework, satisfies lets you validate internal implementation objects against exported contracts without losing internal precision. This is especially important when the implementation needs to derive types from the object itself.
// A mini plugin framework
interface PluginContract {
name: string;
hooks: {
onInit?: () => void;
onDestroy?: () => void;
};
}
// Internal registry — validate contract but keep literal name types
const BUILT_IN_PLUGINS = {
logger: {
name: "logger",
hooks: {
onInit: () => console.log("Logger init"),
onDestroy: () => console.log("Logger destroyed"),
},
},
metrics: {
name: "metrics",
hooks: {
onInit: () => console.log("Metrics init"),
},
},
} as const satisfies Record<string, PluginContract>;
// BUILT_IN_PLUGINS.logger.name is "logger" (literal), not string
// Useful for routing, plugin lookup, code splitting, etc.
type BuiltInPluginName = keyof typeof BUILT_IN_PLUGINS;
// "logger" | "metrics"Summary
satisfies validates that a value matches a type without widening the inferred type
Unlike a type annotation, satisfies preserves literal and per-property types
as const satisfies Type freezes literals AND validates the schema in one expression
Ideal for configuration objects, route tables, design tokens, and enum-like maps
Catches shape errors (missing keys, wrong types) at the point of definition
Available from TypeScript 4.9 onward
Combines naturally with discriminated unions to preserve the literal discriminant type
Works on arrays too — validates each element while preserving tuple-level literal types
In library code, validates implementation objects against contracts while retaining literal precision
In design systems, it enforces complete token coverage while keeping literal precision