The satisfies operator validates types while preserving the most specific inferred type. Here's how to use it.
Basic Usage#
1// Without satisfies - type annotation widens
2const colors1: Record<string, string> = {
3 red: '#ff0000',
4 green: '#00ff00',
5 blue: '#0000ff',
6};
7// colors1.red is string
8// colors1.purple is string (no error!)
9
10// With satisfies - validates but preserves literal types
11const colors2 = {
12 red: '#ff0000',
13 green: '#00ff00',
14 blue: '#0000ff',
15} satisfies Record<string, string>;
16
17// colors2.red is '#ff0000' (literal type)
18// colors2.purple // Error: Property 'purple' does not exist
19
20// Keys are also preserved
21type ColorKeys = keyof typeof colors2; // 'red' | 'green' | 'blue'Type Validation#
1// Ensure object matches interface
2interface Config {
3 apiUrl: string;
4 timeout: number;
5 retries: number;
6}
7
8const config = {
9 apiUrl: 'https://api.example.com',
10 timeout: 5000,
11 retries: 3,
12} satisfies Config;
13
14// Catches errors
15const badConfig = {
16 apiUrl: 'https://api.example.com',
17 timeout: '5000', // Error: Type 'string' not assignable to 'number'
18 retries: 3,
19} satisfies Config;
20
21// Missing properties
22const incompleteConfig = {
23 apiUrl: 'https://api.example.com',
24 // Error: Property 'timeout' is missing
25} satisfies Config;Preserving Literal Types#
1// Route definitions
2type Route = {
3 path: string;
4 method: 'GET' | 'POST' | 'PUT' | 'DELETE';
5};
6
7// Without satisfies
8const routes1: Route[] = [
9 { path: '/users', method: 'GET' },
10 { path: '/users', method: 'POST' },
11];
12// routes1[0].method is 'GET' | 'POST' | 'PUT' | 'DELETE'
13
14// With satisfies
15const routes2 = [
16 { path: '/users', method: 'GET' },
17 { path: '/users', method: 'POST' },
18] satisfies Route[];
19
20// routes2[0].method is 'GET' (literal type preserved)
21// routes2[1].method is 'POST'
22
23// Error handling preserves exact values
24type ErrorCode = 'NOT_FOUND' | 'UNAUTHORIZED' | 'SERVER_ERROR';
25
26const errorMessages = {
27 NOT_FOUND: 'Resource not found',
28 UNAUTHORIZED: 'Authentication required',
29 SERVER_ERROR: 'Internal server error',
30} satisfies Record<ErrorCode, string>;
31
32// Type is { NOT_FOUND: string; UNAUTHORIZED: string; SERVER_ERROR: string }
33// Keys are exactly ErrorCode, not just stringUnion Types#
1// Mixed value types
2type Value = string | number | boolean;
3
4const settings = {
5 name: 'My App',
6 port: 3000,
7 debug: true,
8} satisfies Record<string, Value>;
9
10// Each property keeps its specific type
11settings.name; // string (not string | number | boolean)
12settings.port; // number
13settings.debug; // boolean
14
15// Array with union
16type Item = { type: 'text'; content: string } | { type: 'image'; src: string };
17
18const items = [
19 { type: 'text', content: 'Hello' },
20 { type: 'image', src: '/photo.jpg' },
21] satisfies Item[];
22
23// items[0] is { type: 'text'; content: string }
24// items[1] is { type: 'image'; src: string }Configuration Objects#
1// Theme configuration
2type ThemeValue = string | number | { light: string; dark: string };
3
4const theme = {
5 colors: {
6 primary: { light: '#007bff', dark: '#4da3ff' },
7 background: { light: '#ffffff', dark: '#1a1a1a' },
8 },
9 spacing: {
10 small: 8,
11 medium: 16,
12 large: 24,
13 },
14 fontFamily: 'Inter, sans-serif',
15} satisfies Record<string, Record<string, ThemeValue> | ThemeValue>;
16
17// Access with full type information
18theme.colors.primary.light; // string
19theme.spacing.small; // number
20theme.fontFamily; // string
21
22// API configuration
23type Endpoint = {
24 url: string;
25 method: 'GET' | 'POST';
26 auth?: boolean;
27};
28
29const endpoints = {
30 getUsers: { url: '/users', method: 'GET' },
31 createUser: { url: '/users', method: 'POST', auth: true },
32 getProfile: { url: '/profile', method: 'GET', auth: true },
33} satisfies Record<string, Endpoint>;
34
35// endpoints.getUsers.method is 'GET', not 'GET' | 'POST'With as const#
1// Combining satisfies and as const
2type Status = 'pending' | 'active' | 'completed';
3
4// as const makes everything readonly and literal
5const statuses = {
6 PENDING: 'pending',
7 ACTIVE: 'active',
8 COMPLETED: 'completed',
9} as const satisfies Record<string, Status>;
10
11// Type is:
12// {
13// readonly PENDING: 'pending';
14// readonly ACTIVE: 'active';
15// readonly COMPLETED: 'completed';
16// }
17
18// Enum-like pattern
19const HttpStatus = {
20 OK: 200,
21 CREATED: 201,
22 BAD_REQUEST: 400,
23 NOT_FOUND: 404,
24 SERVER_ERROR: 500,
25} as const satisfies Record<string, number>;
26
27type HttpStatusCode = (typeof HttpStatus)[keyof typeof HttpStatus];
28// 200 | 201 | 400 | 404 | 500Event Handlers#
1type EventHandler<T> = (event: T) => void;
2
3interface Events {
4 click: MouseEvent;
5 keydown: KeyboardEvent;
6 scroll: Event;
7}
8
9const handlers = {
10 click: (e) => console.log(e.clientX),
11 keydown: (e) => console.log(e.key),
12 scroll: (e) => console.log(e.target),
13} satisfies { [K in keyof Events]: EventHandler<Events[K]> };
14
15// Parameter types are inferred correctly
16// handlers.click expects MouseEvent
17// handlers.keydown expects KeyboardEventFunction Return Types#
1// Factory function
2type WidgetConfig = {
3 name: string;
4 render: () => void;
5};
6
7function createWidget<T extends WidgetConfig>(config: T) {
8 return config;
9}
10
11// Using satisfies for validation
12const widget = createWidget({
13 name: 'Counter',
14 count: 0,
15 render() {
16 console.log(this.count);
17 },
18} satisfies WidgetConfig & { count: number });
19
20// widget has both WidgetConfig properties and count
21widget.count; // number
22
23// Middleware pattern
24type Middleware = (req: Request, res: Response, next: () => void) => void;
25
26const authMiddleware = ((req, res, next) => {
27 // req, res, next are properly typed
28 if (!req.headers.authorization) {
29 res.status(401).send('Unauthorized');
30 return;
31 }
32 next();
33}) satisfies Middleware;Validation Schemas#
1// Form validation schema
2type Validator<T> = {
3 validate: (value: unknown) => value is T;
4 message: string;
5};
6
7const validators = {
8 email: {
9 validate: (v): v is string =>
10 typeof v === 'string' && v.includes('@'),
11 message: 'Invalid email address',
12 },
13 age: {
14 validate: (v): v is number =>
15 typeof v === 'number' && v >= 0 && v <= 120,
16 message: 'Age must be between 0 and 120',
17 },
18 required: {
19 validate: (v): v is string =>
20 typeof v === 'string' && v.length > 0,
21 message: 'This field is required',
22 },
23} satisfies Record<string, Validator<unknown>>;
24
25// Each validator preserves its specific type parameterProps Definition#
1// Component props
2type ButtonVariant = 'primary' | 'secondary' | 'danger';
3
4const buttonStyles = {
5 primary: {
6 background: '#007bff',
7 color: '#ffffff',
8 hoverBackground: '#0056b3',
9 },
10 secondary: {
11 background: '#6c757d',
12 color: '#ffffff',
13 hoverBackground: '#545b62',
14 },
15 danger: {
16 background: '#dc3545',
17 color: '#ffffff',
18 hoverBackground: '#c82333',
19 },
20} satisfies Record<
21 ButtonVariant,
22 { background: string; color: string; hoverBackground: string }
23>;
24
25// Type-safe access
26function getButtonStyle(variant: ButtonVariant) {
27 return buttonStyles[variant];
28 // Return type preserves the exact structure
29}Best Practices#
Usage:
✓ Validate object structure
✓ Preserve literal types
✓ Keep specific key types
✓ Combine with as const
Benefits:
✓ Better type inference
✓ Compile-time validation
✓ Autocomplete works
✓ Refactoring safety
vs Type Annotation:
✓ satisfies: validates + preserves specifics
✗ Annotation: validates + widens type
Avoid:
✗ Using when annotation suffices
✗ Complex nested satisfies
✗ Forgetting it's TypeScript 4.9+
✗ Using for simple primitives
Conclusion#
The satisfies operator validates that a value matches a type while preserving the most specific inferred type. Use it for configuration objects, route definitions, and anywhere you want both type safety and precise type inference. It's particularly powerful with literal types and union types.