Back to Blog
TypeScriptsatisfiesType SafetyInference

TypeScript satisfies Operator Guide

Master the TypeScript satisfies operator for type validation while preserving inferred types.

B
Bootspring Team
Engineering
January 10, 2020
6 min read

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 string

Union 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 | 500

Event 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 KeyboardEvent

Function 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 parameter

Props 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.

Share this article

Help spread the word about Bootspring