TypeScript's satisfies operator (introduced in 4.9) provides type validation without type widening. You can ensure a value matches a type while keeping the most specific type possible. This solves a common tension between safety and precise inference.
The problem
Consider this configuration object:
type Config = {
theme: 'light' | 'dark';
language: string;
};
// With type annotation
const config: Config = {
theme: 'dark',
language: 'en',
};
// config.theme is 'light' | 'dark', not 'dark'
// We lost the literal type!
The annotation widens config.theme from 'dark' to 'light' | 'dark'. Sometimes you need the literal.
Enter satisfies
const config = {
theme: 'dark',
language: 'en',
} satisfies Config;
// config.theme is 'dark' (literal preserved!)
// TypeScript still validates against Config
You get validation AND precise inference.
How satisfies differs from annotation
| Approach | Validation | Resulting type |
|---|---|---|
const x: Type = value |
Yes | Type (widened) |
const x = value satisfies Type |
Yes | Inferred (narrow) |
const x = value as Type |
No | Type (trusted) |
type Theme = 'light' | 'dark';
// Annotation: widened to Theme
const theme1: Theme = 'dark';
// theme1: Theme
// Satisfies: stays literal
const theme2 = 'dark' satisfies Theme;
// theme2: 'dark'
// Assertion: no validation!
const theme3 = 'oops' as Theme;
// No error, theme3: Theme
Use case: configuration objects
type ButtonVariants = {
[key: string]: {
background: string;
color: string;
border?: string;
};
};
const variants = {
primary: {
background: 'blue',
color: 'white',
},
secondary: {
background: 'gray',
color: 'black',
border: '1px solid black',
},
} satisfies ButtonVariants;
// variants.primary is precisely typed
// variants.primary.background is 'blue', not string
You validate the shape while keeping specific property types.
Use case: route definitions
type Route = {
path: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
};
const routes = {
getUser: { path: '/users/:id', method: 'GET' },
createUser: { path: '/users', method: 'POST' },
updateUser: { path: '/users/:id', method: 'PUT' },
} satisfies Record<string, Route>;
// routes.getUser.method is 'GET', not 'GET' | 'POST' | ...
// Autocomplete works on route keys
Use case: theme definitions
type ColorScale = Record<number, string>;
const colors = {
gray: {
100: '#f7fafc',
200: '#edf2f7',
300: '#e2e8f0',
400: '#cbd5e0',
500: '#a0aec0',
},
blue: {
100: '#ebf8ff',
200: '#bee3f8',
300: '#90cdf4',
},
} satisfies Record<string, ColorScale>;
// TypeScript knows colors.gray exists
// colors.gray[100] is '#f7fafc'
// colors.gray[600] would error - doesn't exist!
Use case: enum-like objects
type Status = 'pending' | 'active' | 'completed';
const STATUS = {
PENDING: 'pending',
ACTIVE: 'active',
COMPLETED: 'completed',
} satisfies Record<string, Status>;
// STATUS.PENDING is 'pending', not Status
// Useful for switch exhaustiveness
Satisfies with arrays
type MenuItem = {
label: string;
href: string;
};
const menu = [
{ label: 'Home', href: '/' },
{ label: 'About', href: '/about' },
{ label: 'Contact', href: '/contact' },
] satisfies MenuItem[];
// menu[0].label is 'Home'
// But still validated as MenuItem[]
Combining with as const
For fully immutable literals:
const config = {
api: 'https://api.example.com',
timeout: 5000,
} as const satisfies {
api: string;
timeout: number;
};
// config.api is 'https://api.example.com' (readonly)
// config.timeout is 5000 (readonly)
// And validated against the type
Error detection
satisfies catches errors that as would miss:
type Config = {
debug: boolean;
level: 'info' | 'warn' | 'error';
};
// satisfies catches the error
const config = {
debug: true,
level: 'verbose', // Error: 'verbose' not assignable to 'info' | 'warn' | 'error'
} satisfies Config;
// as would hide it
const config2 = {
debug: true,
level: 'verbose', // No error! Unsafe
} as Config;
When to use which
| Scenario | Use |
|---|---|
| Function parameters | Type annotation |
| Return types | Type annotation |
| Variables with specific type | satisfies |
| Config objects | satisfies |
| Type narrowing impossible | as (carefully) |
| Generic constraints | Type annotation |
Practical examples
API response types
type ApiResponse<T> = {
data: T;
status: 'success' | 'error';
};
const response = {
data: { id: 1, name: 'Alice' },
status: 'success',
} satisfies ApiResponse<{ id: number; name: string }>;
// response.status is 'success', not 'success' | 'error'
Form defaults
type FormValues = {
name: string;
email: string;
subscribe: boolean;
};
const defaults = {
name: '',
email: '',
subscribe: false,
} satisfies FormValues;
// Validated shape, precise types
// defaults.subscribe is false (boolean literal)
Event handlers map
type EventHandler = (event: Event) => void;
const handlers = {
click: (e) => console.log('clicked', e),
scroll: (e) => console.log('scrolled', e),
resize: (e) => console.log('resized', e),
} satisfies Record<string, EventHandler>;
// handlers.click is validated
// Keys are autocomplete-able
Limitations
satisfies doesn't:
- Create a new type (just validates)
- Work with generics in all contexts
- Replace proper type annotations for APIs
// Still need annotation for function params
function process(config: Config) { ... }
// satisfies is for values, not types
type MyConfig = ... satisfies SomeType; // Error
Summary
The satisfies operator validates that values match types without widening them. Use it for configuration objects, route maps, theme definitions, and anywhere you need both type safety and literal inference. It catches errors that as assertions would miss while preserving the precise types that annotations would lose.
