7.3 The satisfies Operator
There are several ways to specify types when writing TypeScript code: type annotations (: Type), type assertions (as Type), and the satisfies operator added in TypeScript 4.9. While they look similar, they behave in fundamentally different ways. satisfies means: "check that this value satisfies this type, but keep the inferred literal type as-is."
What Is satisfies?
The satisfies operator does two things at once.
- Type checking: Verifies at compile time that the value satisfies the specified type.
- Literal type preservation: Does not lose the concrete (narrow) inferred type of the value.
type Color = "red" | "green" | "blue";
interface Palette {
primary: Color | [number, number, number];
secondary: Color | [number, number, number];
background: Color | [number, number, number];
}
// Type annotation: type check yes, literal type no (widened)
const palette1: Palette = {
primary: "red",
secondary: [255, 128, 0],
background: "blue",
};
// palette1.primary type: Color | [number, number, number]
// The fact that it is "red" is lost
// satisfies: type check yes, literal type yes (preserved)
const palette2 = {
primary: "red",
secondary: [255, 128, 0],
background: "blue",
} satisfies Palette;
// palette2.primary type: "red"
// palette2.secondary type: [number, number, number]
// palette2.background type: "blue"
// Since the literal type is preserved, array methods are also safe to use
palette2.secondary.map(v => v * 2); // OK — TypeScript knows it's an array
// palette1.secondary.map(v => v * 2); // Error — it's Color | [...], so map might not exist
Differences from as
as (type assertion) forces TypeScript to believe "I assert this is this type, trust me." It bypasses type checking.
type Status = "active" | "inactive" | "pending";
// as assertion: no type check, even wrong values pass
const status1 = "actve" as Status; // Typo, but no error!
// satisfies: type check on, wrong values are errors
// const status2 = "actve" satisfies Status; // Error: "actve" is not in Status
const status2 = "active" satisfies Status; // OK
// Dangerous pattern with as
interface User {
id: number;
name: string;
email: string;
}
// as can assert to a completely different type (double assertion)
const fakeUser = {} as User; // Compiles fine, runtime error risk
// satisfies requires the actual value to satisfy the type
// const fakeUser2 = {} satisfies User; // Error: id, name, email are all missing
// The only legitimate use case for as: when type narrowing is not possible
function processValue(value: unknown): string {
if (typeof value === "string") return value;
return (value as string); // When a runtime check has already been done
}
satisfies vs as in Practice
// Configuration object example
type AppConfig = {
port: number;
host: string;
debug: boolean;
features: string[];
};
// as approach: not type-safe
const config1 = {
port: 3000,
host: "localhost",
debug: true,
features: ["auth", "logging"],
unknownProp: "oops", // No error! as does not catch this
} as AppConfig;
// satisfies approach: includes excess property checking
// const config2 = {
// port: 3000,
// host: "localhost",
// debug: true,
// features: ["auth", "logging"],
// unknownProp: "oops", // Error: unknownProp does not exist in AppConfig
// } satisfies AppConfig;
// Correct usage
const config2 = {
port: 3000,
host: "localhost",
debug: true,
features: ["auth", "logging"],
} satisfies AppConfig;
// Literal type preserved: 3000, not just number
type ConfigPort = typeof config2.port; // 3000 (literal)
type ConfigHost = typeof config2.host; // "localhost" (literal)
Differences from Explicit Type Annotations
A type annotation (: Type) widens the variable's type to the declared type. The inferred literal type is lost.
type Direction = "north" | "south" | "east" | "west";
// Annotation: type is widened to Direction
const dir1: Direction = "north";
type Dir1Type = typeof dir1; // Direction (not a literal)
// satisfies: checks Direction constraint + preserves the literal
const dir2 = "north" satisfies Direction;
type Dir2Type = typeof dir2; // "north" (literal)
// Difference with objects
interface RouteConfig {
path: string;
method: "GET" | "POST" | "PUT" | "DELETE";
handler: string;
}
// Annotation: all properties are widened according to RouteConfig
const route1: RouteConfig = {
path: "/users",
method: "GET",
handler: "getUsers",
};
type Route1Method = typeof route1.method; // "GET" | "POST" | "PUT" | "DELETE"
// satisfies: preserves literal type of each value
const route2 = {
path: "/users",
method: "GET",
handler: "getUsers",
} satisfies RouteConfig;
type Route2Method = typeof route2.method; // "GET" (literal)
type Route2Path = typeof route2.path; // string (literal strings infer as string)
// Usage: exhaustive check in a switch over the route method
function handleRoute(method: typeof route2.method): void {
// method is "GET", so other cases are unreachable
if (method === "GET") {
console.log("handling GET");
}
}
Combining satisfies with as const
Combining as const and satisfies lets you keep the narrowest possible type while also enforcing a type constraint.
type ThemeColor = string;
interface Theme {
colors: Record<string, ThemeColor>;
spacing: Record<string, number | string>;
breakpoints: Record<string, number>;
}
// as const only: no type constraint
const themeConst = {
colors: { primary: "#007bff", secondary: "#6c757d" },
spacing: { sm: 8, md: 16, lg: 24 },
breakpoints: { mobile: 768, tablet: 1024 },
} as const;
// satisfies only: type constraint, partial literal preservation
const themeSatisfies = {
colors: { primary: "#007bff", secondary: "#6c757d" },
spacing: { sm: 8, md: 16, lg: 24 },
breakpoints: { mobile: 768, tablet: 1024 },
} satisfies Theme;
// as const + satisfies combination: maximally narrow type + type constraint
const theme = {
colors: { primary: "#007bff", secondary: "#6c757d" },
spacing: { sm: 8, md: 16, lg: 24 },
breakpoints: { mobile: 768, tablet: 1024 },
} as const satisfies Theme;
// Result
type PrimaryColor = typeof theme.colors.primary; // "#007bff" (literal)
type MobileBreakpoint = typeof theme.breakpoints.mobile; // 768 (literal)
// Errors are caught too
// const badTheme = {
// colors: { primary: 12345 }, // Error: number is not ThemeColor (string)
// } as const satisfies Theme;
// Practical example: icon map
type IconName = "home" | "settings" | "user" | "bell";
const ICONS = {
home: "/icons/home.svg",
settings: "/icons/settings.svg",
user: "/icons/user.svg",
bell: "/icons/bell.svg",
} as const satisfies Record<IconName, string>;
// Keys outside IconName are errors
// const BAD_ICONS = {
// invalid: "/icons/invalid.svg", // Error
// } satisfies Record<IconName, string>;
type IconUrl = typeof ICONS.home; // "/icons/home.svg" (literal)
Practical Example 1: Type-Safe Configuration Objects
// Type-safely defining per-environment configuration
type Environment = "development" | "staging" | "production";
interface DatabaseConfig {
host: string;
port: number;
name: string;
ssl: boolean;
poolSize: number;
}
interface ServerConfig {
host: string;
port: number;
cors: {
origins: string[];
methods: Array<"GET" | "POST" | "PUT" | "DELETE" | "PATCH">;
};
}
interface AppConfig {
env: Environment;
server: ServerConfig;
database: DatabaseConfig;
features: {
auth: boolean;
logging: boolean;
rateLimiting: boolean;
};
}
const devConfig = {
env: "development",
server: {
host: "localhost",
port: 3000,
cors: {
origins: ["http://localhost:3000", "http://localhost:5173"],
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
},
},
database: {
host: "localhost",
port: 5432,
name: "myapp_dev",
ssl: false,
poolSize: 5,
},
features: {
auth: true,
logging: true,
rateLimiting: false,
},
} satisfies AppConfig;
// Literal types are preserved
type DevEnv = typeof devConfig.env; // "development"
type DevPort = typeof devConfig.server.port; // 3000
type DevDbSsl = typeof devConfig.database.ssl; // false
// Config function: return type is inferred precisely
function getConfig(env: Environment) {
const configs = {
development: devConfig,
staging: { ...devConfig, env: "staging" as const },
production: {
...devConfig,
env: "production" as const,
database: { ...devConfig.database, ssl: true, poolSize: 20 },
},
} satisfies Record<Environment, AppConfig>;
return configs[env];
}
const prodConfig = getConfig("production");
type ProdSsl = typeof prodConfig.database.ssl; // boolean (widened by satisfies, but still type-safe)
Practical Example 2: Route Map Definition
// Type-safe route registry
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
interface RouteDefinition {
method: HttpMethod;
path: string;
description: string;
requiresAuth: boolean;
}
type RouteRegistry = Record<string, RouteDefinition>;
const API_ROUTES = {
listUsers: {
method: "GET",
path: "/api/users",
description: "Get all users",
requiresAuth: true,
},
createUser: {
method: "POST",
path: "/api/users",
description: "Create a new user",
requiresAuth: true,
},
getUser: {
method: "GET",
path: "/api/users/:id",
description: "Get user by ID",
requiresAuth: true,
},
updateUser: {
method: "PUT",
path: "/api/users/:id",
description: "Update user",
requiresAuth: true,
},
deleteUser: {
method: "DELETE",
path: "/api/users/:id",
description: "Delete user",
requiresAuth: true,
},
health: {
method: "GET",
path: "/health",
description: "Health check",
requiresAuth: false,
},
} satisfies RouteRegistry;
// The method of each route is preserved as a literal type
type ListUsersMethod = typeof API_ROUTES.listUsers.method; // "GET"
type CreateUserMethod = typeof API_ROUTES.createUser.method; // "POST"
// An invalid method is a compile error
// const BAD_ROUTES = {
// badRoute: { method: "INVALID", path: "/", ... } // Error
// } satisfies RouteRegistry;
// Using route names as keys
type RouteName = keyof typeof API_ROUTES;
// "listUsers" | "createUser" | "getUser" | "updateUser" | "deleteUser" | "health"
// Filtering only routes that require authentication (accurate type inference thanks to satisfies)
type AuthenticatedRoutes = {
[K in RouteName]: typeof API_ROUTES[K]["requiresAuth"] extends true ? K : never;
}[RouteName];
// "listUsers" | "createUser" | "getUser" | "updateUser" | "deleteUser"
Practical Example 3: Palette Color Types
// Type-safely defining design tokens
type HexColor = `#${string}`;
type RgbColor = `rgb(${number}, ${number}, ${number})`;
type HslColor = `hsl(${number}, ${number}%, ${number}%)`;
type CssColor = HexColor | RgbColor | HslColor;
interface ColorShades {
50: CssColor;
100: CssColor;
200: CssColor;
300: CssColor;
400: CssColor;
500: CssColor;
600: CssColor;
700: CssColor;
800: CssColor;
900: CssColor;
}
interface DesignSystem {
colors: {
primary: ColorShades;
neutral: ColorShades;
error: Partial<ColorShades>;
success: Partial<ColorShades>;
};
}
const designTokens = {
colors: {
primary: {
50: "#eff6ff",
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
900: "#1e3a8a",
},
neutral: {
50: "#f9fafb",
100: "#f3f4f6",
200: "#e5e7eb",
300: "#d1d5db",
400: "#9ca3af",
500: "#6b7280",
600: "#4b5563",
700: "#374151",
800: "#1f2937",
900: "#111827",
},
error: {
500: "#ef4444",
700: "#b91c1c",
},
success: {
500: "#22c55e",
700: "#15803d",
},
},
} satisfies DesignSystem;
// Accessing color values with literal types
type Primary500 = typeof designTokens.colors.primary["500"]; // "#3b82f6"
// Color utility function (precise type inference thanks to literal types)
function getColor<
Category extends keyof typeof designTokens.colors,
Shade extends keyof typeof designTokens.colors[Category]
>(
category: Category,
shade: Shade
): typeof designTokens.colors[Category][Shade] {
return designTokens.colors[category][shade] as any;
}
const primaryBlue = getColor("primary", "500");
// Type: "#3b82f6"
Pro Tips
Limits of satisfies: Inference Depth
satisfies does not widen type inference, but it may behave unexpectedly in deeply nested structures.
// Array contents may not be preserved as literal types
interface Config {
tags: string[];
}
const config = {
tags: ["typescript", "react", "node"],
} satisfies Config;
type Tags = typeof config.tags; // string[] (not a literal array)
// Solved with as const + satisfies
const configConst = {
tags: ["typescript", "react", "node"],
} as const satisfies Config;
type TagsConst = typeof configConst.tags;
// readonly ["typescript", "react", "node"] (tuple literal)
// Limits with function type inference
interface Actions {
onClick: (event: MouseEvent) => void;
}
const actions = {
onClick: (e) => console.log(e.clientX), // e is inferred as MouseEvent
} satisfies Actions;
// Parameter types inside satisfies are inferred contextually — this case works correctly
When to Choose Annotation vs satisfies
// When to choose annotation:
// 1. When you intentionally want to widen the variable's type
let message: string = "hello"; // will reassign to another string later
// message = "world"; // OK
// 2. When the inferred type is too narrow and causes problems
function processStatus(status: string): void {
// various processing inside
}
const STATUS: string = "active"; // needs to be string to pass to processStatus
processStatus(STATUS); // OK
// When to choose satisfies:
// 1. When you need both type safety checking and precise type inference
const httpMethods = ["GET", "POST", "PUT"] satisfies Array<HttpMethod>;
// httpMethods[0] is "GET" (literal) | "POST" | "PUT" — an array but content types are preserved
// 2. When you want to use keys in a config object as types
const featureFlags = {
darkMode: false,
notifications: true,
betaFeatures: false,
} satisfies Record<string, boolean>;
type FeatureFlag = keyof typeof featureFlags;
// "darkMode" | "notifications" | "betaFeatures" (keys preserved thanks to satisfies)
// With annotation it would be: string (keys are lost)
// 3. When defining compile-time constants
const MAX_RETRY = 3 satisfies number;
// typeof MAX_RETRY is 3 (literal), but validated to be a number
Summary
| Syntax | Type Check | Literal Preserved | Wrong Value | Primary Use |
|---|---|---|---|---|
: Type (annotation) | Yes | No (widened) | Error | Variable type declaration |
as Type (assertion) | No (forced) | No | No error | Type coercion |
satisfies Type | Yes | Yes | Error | Config objects, constants |
as const satisfies Type | Yes | Yes (maximum) | Error | Immutable constants |
In the next chapter, we look at TypeScript 5.x new features such as const type parameters, NoInfer<T>, and the using keyword, and learn about the powerful type inference and resource management capabilities provided by modern TypeScript.