satisfies on a Config Object Without Widening
Check a config object against a type with satisfies and keep its precise keys and literal values. After this, a typo in a key is caught and the object still autocompletes.
Skip ahead: If you already know satisfies and want the lookup example, skip to "Step 3: the keys stay known", then continue.
You have a config object. You want the compiler to check it against a type, and you also want it to remember exactly what you wrote: the keys, the literal values. A type annotation does the first and loses the second. satisfies does both. This page uses a routes table, because that is where the difference shows up in daily work.
Step 1: the annotation version
type Route = { path: string; auth?: boolean };
const routes: Record<string, Route> = {
home: { path: "/" },
admin: { path: "/admin", auth: true },
};
console.log(routes.hme.path);
This compiles, and routes.hme is a typo. Record<string, Route> says "any string is a key", so the compiler stopped remembering that you wrote home and admin. Without noUncheckedIndexedAccess, every key you type is Route and none is undefined. At run time, the line throws.
Step 2: replace the annotation with satisfies
type Route = { path: string; auth?: boolean };
const routes = {
home: { path: "/" },
admin: { path: "/admin", auth: true },
} satisfies Record<string, Route>;
console.log(routes.hme.path);
satisfied.ts(8,20): error TS2551: Property 'hme' does not exist on type '{ home: { path: string; }; admin: { path: string; auth: true; }; }'. Did you mean 'home'?
Now the typo fails, and the message even suggests the right key. The compiler still checks that every entry is a Route: the value on the right was compared to the type, and then the type of the value itself was kept.
Step 3: the keys stay known
That gives you types you can derive from the object, so the list of routes lives in one place.
type Route = { path: string; auth?: boolean };
const routes = {
home: { path: "/" },
admin: { path: "/admin", auth: true },
profile: { path: "/users/me", auth: true },
} satisfies Record<string, Route>;
type RouteName = keyof typeof routes;
function pathFor(name: RouteName): string {
return routes[name].path;
}
const names = Object.keys(routes) as RouteName[];
console.log(pathFor("profile"));
console.log(names);
/users/me
[ 'home', 'admin', 'profile' ]
RouteName is "home" | "admin" | "profile", built from the object. Add a route to the table and the type grows with it. Object.keys returns string[] in TypeScript, so the one cast to RouteName[] is the usual price. It is safe here, because you know the object has no other keys.
Check what the compiler thinks, using the wrong-value trick.
type Route = { path: string; auth?: boolean };
const routes = {
home: { path: "/" },
admin: { path: "/admin", auth: true },
} satisfies Record<string, Route>;
const a: keyof typeof routes = "login";
const b: typeof routes.admin.auth = "yes";
inspect.ts(8,7): error TS2322: Type '"login"' is not assignable to type '"admin" | "home"'.
inspect.ts(9,7): error TS2322: Type '"yes"' is not assignable to type 'true'.
The first error lists the keys as the compiler knows them. The second shows that auth on the admin route is the literal true, not boolean. The value was read from your object, not from the Route type.
Step 4: it still catches wrong entries
satisfies has not stopped checking. Each of these is a mistake in the table.
type Route = { path: string; auth?: boolean };
const routes = {
home: { path: "/" },
admin: { path: "/admin", auth: "yes" },
profile: { url: "/users/me" },
} satisfies Record<string, Route>;
bad_entries.ts(5,28): error TS2322: Type 'string' is not assignable to type 'boolean | undefined'.
bad_entries.ts(6,14): error TS2353: Object literal may only specify known properties, and 'url' does not exist in type 'Route'.
A wrong type for auth, and a misspelled property name (url for path), both fail, with the entry in the message.
💡 Tip: Use an annotation when the type is the contract, for example a function parameter or an exported constant that other code must treat as a plain Record. Use satisfies for a local table that you read the keys from.
Common mistakes
- Writing
as Record<string, Route>instead. A cast does not check the entries. - Expecting
satisfiesto make the object readonly. It does not. Addas constwhen the values must not change, as in the next tutorial. - Using it on a value that has nothing to derive. If you never read the keys or literals back, the plain annotation is simpler.
Related course: TypeScript Advanced.
About
Check a config object against a type with satisfies and keep its precise keys and literal values. After this, a typo in a key is caught and the object still autocompletes.
- Responsible
- John G
- Last Update
- 10/18/2026
- Reading Time
- 4 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.