The problem satisfies solves
With a normal type annotation (: Colors), you lose the exact literal type – TypeScript widens it to the union. This causes issues when you need the literal for autocomplete or inference elsewhere.
typescript
1type Colors = 'red' | 'green' | 'blue';
2
3// ❌ Type annotation – widens to 'Colors', not literal
4const color1: Colors = 'red';
5// color1 is 'red' | 'green' | 'blue' (union, not literal)
6
7// ✅ satisfies – keeps literal 'red'
8const color2 = 'red' satisfies Colors;
9// color2 is 'red' (the literal)
1type Colors = 'red' | 'green' | 'blue';
2
3// ❌ Type annotation – widens to 'Colors', not literal
4const color1: Colors = 'red';
5// color1 is 'red' | 'green' | 'blue' (union, not literal)
6
7// ✅ satisfies – keeps literal 'red'
8const color2 = 'red' satisfies Colors;
9// color2 is 'red' (the literal)
Real‑world example: object properties
typescript
1const palette = {
2 primary: '#ff0000',
3 secondary: '#00ff00',
4 accent: 'blue' // typo – should be a hex code
5} satisfies Record<string, `#${string}`>;
6
7// The typo `'blue'` errors ✅ but palette still has its original shape
1const palette = {
2 primary: '#ff0000',
3 secondary: '#00ff00',
4 accent: 'blue' // typo – should be a hex code
5} satisfies Record<string, `#${string}`>;
6
7// The typo `'blue'` errors ✅ but palette still has its original shape
vs type annotation on object
typescript
1type Palette = Record<string, `#${string}`>;
2
3// ❌ Annotation loses all key names
4const p1: Palette = {
5 primary: '#ff0000' // p1.primary is string (no literal key info)
6};
7
8// ✅ satisfies keeps key names and checks values
9const p2 = {
10 primary: '#ff0000'
11} satisfies Palette;
12// p2.primary is '#ff0000' (literal), and autocomplete works
1type Palette = Record<string, `#${string}`>;
2
3// ❌ Annotation loses all key names
4const p1: Palette = {
5 primary: '#ff0000' // p1.primary is string (no literal key info)
6};
7
8// ✅ satisfies keeps key names and checks values
9const p2 = {
10 primary: '#ff0000'
11} satisfies Palette;
12// p2.primary is '#ff0000' (literal), and autocomplete works
Key differences
| Feature | Type annotation (: T) | satisfies T |
|---|
| Enforces type | ✅ | ✅ |
| Preserves literal types | ❌ (widens) | ✅ (narrow) |
| Excess property check | yes (sometimes) | yes |
| Keeps original inference | ❌ | ✅ |
| Works with as const | ❌ (overrides) | ✅ (adds on top) |
Bonus: satisfies + as const
typescript
1const config = {
2 apiUrl: 'https://api.example.com',
3 timeout: 5000
4} as const satisfies { apiUrl: string; timeout: number };
5// Both `as const` (makes readonly) and `satisfies` (checks shape)
1const config = {
2 apiUrl: 'https://api.example.com',
3 timeout: 5000
4} as const satisfies { apiUrl: string; timeout: number };
5// Both `as const` (makes readonly) and `satisfies` (checks shape)
✅ Use satisfies when: You want type checking without losing the most specific type (e.g., string literals, tuple lengths, object keys). It's perfect for validation of constants.