TypeScript satisfies - Type Safety Without Losing Inference
Validate types without widening them. Keep autocomplete. Catch errors. No type casts needed.
TypeScript 4.9 added satisfies - a way to check that a value matches a type without forcing the value to be that type.
The Problem
You want to validate that an object matches a type, but you also want to keep the specific literal types for autocomplete and type narrowing.
❌ Without satisfies
type Colors = "red" | "green" | "blue";
const palette: Record<string, Colors> = {
primary: "red",
secondary: "green"
};
// ❌ Type is widened to Colors
palette.primary; // type: Colors
// No autocomplete for specific color
// Can't use string methods safely✅ With satisfies
type Colors = "red" | "green" | "blue";
const palette = {
primary: "red",
secondary: "green"
} satisfies Record<string, Colors>;
// ✅ Type is narrowed to literal
palette.primary; // type: "red"
// Full autocomplete
// Safe to use string methodsWhat is satisfies?
satisfies is a TypeScript operator that validates a value against a type without changing the inferred type of that value.
Syntax:
const value = expression satisfies Type;
TypeScript checks that expression is assignable to Type, but the type of value remains the narrowest possible type inferred from expression.
The problem it solves
Before satisfies, you had two bad options:
Option 1: No type annotation
const colors = { primary: "red", secondary: "green", };
Good: Full autocomplete. colors.primary has type "red".
Bad: No validation. If you typo a color, TypeScript does not catch it.
const colors = { primary: "redd", // Typo! But TypeScript is fine with it. secondary: "green", };
Option 2: Type annotation
type Colors = "red" | "green" | "blue"; const colors: Record<string, Colors> = { primary: "red", secondary: "green", };
Good: Validation. TypeScript catches typos.
Bad: Type is widened. colors.primary has type Colors, not "red". You lose literal types and autocomplete.
satisfies gives you both:
const colors = { primary: "red", secondary: "green", } satisfies Record<string, Colors>;
Good: Validation. TypeScript catches typos.
Good: colors.primary has type "red". Autocomplete works.
Real-world examples
Config objects
You have a config with environment-specific values. You want to ensure all required keys are present, but keep the specific values typed.
Without satisfies:
type Config = { apiUrl: string; timeout: number; }; const config: Config = { apiUrl: "https://api.example.com", timeout: 5000, }; // ❌ Type is widened to string config.apiUrl; // type: string // Can't check if it's the production or dev URL
With satisfies:
const config = { apiUrl: "https://api.example.com", timeout: 5000, } satisfies Config; // ✅ Type is literal config.apiUrl; // type: "https://api.example.com" // Full control over exact values
Route definitions
You define routes with specific paths and methods. You want type safety, but also want to reference the exact path strings later.
Without satisfies:
type Route = { path: string; method: "GET" | "POST"; }; const routes: Route[] = [ { path: "/users", method: "GET" }, { path: "/users", method: "POST" }, ]; // ❌ Type is widened routes[0].path; // type: string // Can't use the exact path in other code
With satisfies:
const routes = [ { path: "/users", method: "GET" }, { path: "/users", method: "POST" }, ] satisfies Route[]; // ✅ Type is narrowed routes[0].path; // type: "/users" routes[0].method; // type: "GET" // Full literal types preserved
Theme configuration
You have a design system with color tokens. You want to validate the structure, but keep specific color values for autocomplete.
Without satisfies:
type Theme = Record<string, string | number>; const theme: Theme = { primaryColor: "#3b82f6", fontSize: 16, }; // ❌ Type is widened theme.primaryColor; // type: string | number // Lost specific color value
With satisfies:
const theme = { primaryColor: "#3b82f6", fontSize: 16, } satisfies Theme; // ✅ Type is narrowed theme.primaryColor; // type: "#3b82f6" theme.fontSize; // type: 16 // Autocomplete shows exact values
API response shapes
You fetch data from an API. You want to ensure the shape is correct, but keep the specific values typed for later use.
Without satisfies:
type User = { id: number; name: string; role: "admin" | "user"; }; const mockUser: User = { id: 1, name: "Alice", role: "admin", }; // ❌ Type is widened mockUser.role; // type: "admin" | "user" // Can't narrow based on the specific value
With satisfies:
const mockUser = { id: 1, name: "Alice", role: "admin", } satisfies User; // ✅ Type is narrowed mockUser.role; // type: "admin" // Exact literal type preserved
When NOT to use satisfies
satisfies is great for validation + inference, but not everything.
Skip it when:
- You actually want type widening - if you want
stringinstead of"red", use a type annotation. - The inferred type is good enough - if you don't need validation, just let TypeScript infer.
- You need runtime validation -
satisfiesis compile-time only. Use Zod, io-ts, or similar for runtime checks. - TypeScript < 4.9 -
satisfieswas added in TypeScript 4.9 (November 2022). If you are on an older version, upgrade or use type annotations.
Use it when:
- You want to validate structure without losing literal types
- You need autocomplete for specific values
- You are building config objects, route tables, or design tokens
- You want both type safety and precise inference
Combining with other features
With const assertions
You can combine satisfies with as const for maximum precision:
const routes = [ { path: "/users", method: "GET" }, { path: "/users", method: "POST" }, ] as const satisfies readonly Route[];
Now:
routesis readonly (immutable)- Each
pathandmethodis a literal type - TypeScript validates the structure
This is useful for data that should never change at runtime.
With generics
satisfies works with generic types:
type Response<T> = { data: T; status: number; }; const userResponse = { data: { id: 1, name: "Alice" }, status: 200, } satisfies Response<{ id: number; name: string }>; // ✅ Full inference userResponse.data.id; // type: 1 userResponse.data.name; // type: "Alice"
With mapped types
You can use satisfies to validate mapped types while keeping literal keys:
type Handlers = Record<string, () => void>; const handlers = { onClick: () => console.log("clicked"), onHover: () => console.log("hovered"), } satisfies Handlers; // ✅ Literal keys preserved Object.keys(handlers); // ("onClick" | "onHover")[]
Browser / runtime support
satisfies is a TypeScript-only feature. It compiles away to nothing in JavaScript.
// TypeScript const config = { apiUrl: "https://api.example.com", } satisfies Config; // Compiled JavaScript const config = { apiUrl: "https://api.example.com", };
No runtime overhead. No polyfills needed. Just type checking at compile time.
Common patterns
Config validation
type AppConfig = { api: { url: string; timeout: number }; features: { analytics: boolean; darkMode: boolean }; }; export const config = { api: { url: "https://api.example.com", timeout: 5000, }, features: { analytics: true, darkMode: true, }, } satisfies AppConfig;
Now other files can import config and get full autocomplete + type safety.
Route tables
type Route = { path: string; component: React.ComponentType; }; export const routes = [ { path: "/", component: HomePage }, { path: "/about", component: AboutPage }, ] satisfies Route[];
Each route is validated, but you still get literal types for paths.
Design tokens
type ColorToken = string; type SizeToken = number; export const tokens = { colors: { primary: "#3b82f6", secondary: "#10b981", }, sizes: { sm: 12, md: 16, lg: 20, }, } satisfies { colors: Record<string, ColorToken>; sizes: Record<string, SizeToken>; }; // ✅ Autocomplete for exact values tokens.colors.primary; // type: "#3b82f6"
Try it yourself
Open your TypeScript project and try this:
type Config = { apiUrl: string; timeout: number; }; // Without satisfies const config1: Config = { apiUrl: "https://api.example.com", timeout: 5000, }; config1.apiUrl; // Hover: type is string // With satisfies const config2 = { apiUrl: "https://api.example.com", timeout: 5000, } satisfies Config; config2.apiUrl; // Hover: type is "https://api.example.com"
The difference is immediate. Autocomplete is better. Types are more precise.
Resources
- TypeScript 4.9 Release Notes - official announcement
- TypeScript Handbook: satisfies - detailed guide
Copy. Paste. Type check. Your types are now precise and safe.