← blog
TypeScriptJuly 11, 2026 · 5 min read

Your TypeScript config objects are losing type information. satisfies fixes it.

Type annotations widen your object types and erase the precise values TypeScript inferred. The `satisfies` operator validates against a type without changing what TypeScript keeps — you get the check without the widening.

Parsa Jiravand · Frontend engineer · building bestpractic
Your TypeScript config objects are losing type information. `satisfies` fixes it.

There is a config.ts in most TypeScript projects that looks something like this:

TypeScript
1
2
3
4
5
6
7
8
type Status = 'idle' | 'loading' | 'error' | 'success'; const STATUS_LABELS: Record<Status, string> = { idle: 'Waiting', loading: 'Loading…', error: 'Something went wrong', success: 'Done', };

The annotation validates the object. Miss a key, TypeScript tells you. Use a non-string value, TypeScript tells you. That's the whole point of the annotation. Now look at what you get when you use it:

TypeScript
STATUS_LABELS.idle; // type: string

TypeScript knows the value is a string. It doesn't know it's 'Waiting'. The moment you wrote : Record<Status, string>, TypeScript swapped the inferred type for the declared type and threw away the actual values. You validated the shape, but you gave up precision to do it.

For STATUS_LABELS, that trade is usually fine. But pick a more demanding example — a component icon registry:

TypeScript
1
2
3
4
5
6
7
const ICONS: Record<string, React.ComponentType<any>> = { trash: TrashIcon, edit: EditIcon, copy: CopyIcon, }; ICONS.trash; // type: React.ComponentType<any>

You've lost the specific component types. TrashIcon might have a size prop; TypeScript no longer knows. Any access through ICONS returns the widened union. The annotation bought validation and cost you everything the concrete types were worth.

satisfies arrived in TypeScript 4.9 (November 2022). The syntax is expression satisfies Type. It checks that the left side is compatible with the type on the right — the same check as an annotation — but the resulting inferred type stays as the expression's own type, not the annotation's.

TypeScript
1
2
3
4
5
6
7
const ICONS = { trash: TrashIcon, edit: EditIcon, copy: CopyIcon, } satisfies Record<string, React.ComponentType<any>>; ICONS.trash; // type: typeof TrashIcon — the concrete type, preserved

TypeScript validates that each value is a React.ComponentType. If you put a string in there by mistake, you get an error. But the inferred type of ICONS.trash stays as typeof TrashIcon. The check happened; the widening didn't.

Runs right in your browser — poke at it and watch the concept react live.

Route maps. A central route object is a common pattern, and getting its keys as a typed union is useful for navigation functions:

TypeScript
1
2
3
4
5
6
7
8
// With annotation — keys become string const ROUTES: Record<string, string> = { home: '/', about: '/about', settings: '/settings', }; type RouteName = keyof typeof ROUTES; // string — not what you wanted
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
// With satisfies — keys stay literal const ROUTES = { home: '/', about: '/about', settings: '/settings', } satisfies Record<string, string>; type RouteName = keyof typeof ROUTES; // 'home' | 'about' | 'settings' function navigate(to: RouteName) { /* ... */ } navigate('home'); // fine navigate('contact'); // Error: Argument of type '"contact"' is not assignable to parameter of type 'RouteName'

The annotation validated the paths are strings. satisfies did the same validation and also kept the named keys as a union — so every call to navigate() is checked against the actual routes in your config.

Exhaustive pattern validation. When you have a discriminated union and want to map every case to something:

TypeScript
1
2
3
4
5
6
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number }; const AREA_FORMULAS = { circle: (s: Extract<Shape, { kind: 'circle' }>) => Math.PI * s.radius ** 2, square: (s: Extract<Shape, { kind: 'square' }>) => s.side ** 2, } satisfies { [K in Shape['kind']]: (shape: Extract<Shape, { kind: K }>) => number };

Add a new shape kind to the union and TypeScript immediately tells you AREA_FORMULAS is no longer exhaustive — without you having to look it up.

as is a type assertion: you are telling TypeScript what the type is, skipping its inference. It's useful when TypeScript can't see something you can — reading from the DOM, narrowing after an external type guard — but it provides no validation.

TypeScript
1
2
3
// as — you're asserting, not checking const el = document.getElementById('app') as HTMLDivElement; // TypeScript trusts you; it doesn't verify the element is a div

satisfies is the opposite: TypeScript checks, then steps back. Use as when the compiler lacks information and you have it. Use satisfies when you have information and want the compiler to verify it without losing it. Use : Type annotation when you want to commit to the wider type throughout the file and don't need the narrow inference.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
type Path = '/' | '/about' | '/contact'; // Annotation: validates, then widens — the exact value and keys are gone const routes: Record<string, Path> = { home: '/', about: '/about', contact: '/contact' }; routes.home; // Path — not '/' Object.keys(routes); // string[] type RouteKey = keyof typeof routes; // string — any key is allowed // satisfies: validates, then steps back — the exact value and keys survive const routes = { home: '/', about: '/about', contact: '/contact' } satisfies Record<string, Path>; routes.home; // '/' type RouteKey = keyof typeof routes; // 'home' | 'about' | 'contact'

Two things to notice. First, satisfies keeps the exact keys either way — keyof typeof routes is the real union, so key access and Object.keys stay precise. Second, the value literal '/' only survives because the target's value type (Path) is itself a literal union; against a wider target like Record<string, string> the values would still widen to string. Add a value outside Path to either declaration and TypeScript catches it. The only difference is what comes out the other side.

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.

Use satisfies for any const you define inline and then access by its specific keys or values — config maps, icon registries, route definitions, theme tokens, anything where the precise shape matters downstream. Use annotation when you want to commit to the wider type and everything accessing it should see only that wider type. Use as when TypeScript can't see the correct type on its own.

You've been choosing between safety and precision. satisfies was added so you don't have to.


Thanks for reading! Let's stay connected:

Originally published on dev.to