These features resolve the classic tension between validating a value and keeping the most precise type for it.
An annotation like const routes: Record<string, string> = {...} checks the object but then replaces its type: you lose the specific keys, so routes.hmoe compiles. Leaving the annotation off keeps the keys but checks nothing.
`satisfies` (TS 4.9) does both. expr satisfies T verifies that the expression is assignable to T, including excess property checks, while the expression keeps its own, more precise inferred type:
const routes = { home: "/", about: "/about" } satisfies Record<string, string>;
routes.home; // ok, and routes.hmoe is an errorIt's ideal for config objects, lookup tables and maps of handlers, where you want both validation and autocomplete on the actual keys.
`as const` (a const assertion) tells the compiler to infer the narrowest type: string and number literals stay literal, arrays become readonly tuples, and object properties become readonly. It's how you derive unions from runtime constants. It is not deep-freezing at runtime — just a typing hint.
The two combine well: { ... } as const satisfies Config keeps literal, readonly types and still checks the shape.
const type parameters (TS 5.0) give a generic function the same effect for its arguments without callers writing as const:
function defineRoutes<const T extends readonly string[]>(paths: T): T { return paths; }
defineRoutes(["/", "/about"]); // readonly ["/", "/about"]Use the readonly constraint shown — with a mutable string[] constraint the const inference falls back to plain string[].
The difference from as Type is crucial: an assertion skips checking, satisfies performs it.
Example
type Color = "red" | "green" | "blue";
type RGB = [number, number, number];
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Record<Color, string | RGB>;
palette.green.toUpperCase(); // still known to be a string
palette.red.map(c => c / 255); // still known to be a tuple
const sizes = ["sm", "md", "lg"] as const; // readonly ["sm", "md", "lg"]
type Size = (typeof sizes)[number];
function defineRoutes<const T extends readonly string[]>(paths: T): T {
return paths;
}
const routes = defineRoutes(["/", "/about"]); // readonly ["/", "/about"]
const config = { retries: 3, mode: "fast" } as const satisfies { retries: number; mode: string };
// @ts-expect-error -- "purple" is not a Color key
const bad = { purple: "#800080" } satisfies Partial<Record<Color, string>>;
const s: Size = "md";
console.log(s, routes[1], config.mode);Remember
satisfies Tchecks against T but keeps the inferred typeas constinfers readonly literal types and tuples<const T>gives callersas const-style inference automaticallyas Tsilences checks;satisfies Tperforms them
Watch out for
- Using an annotation on lookup tables and then losing key-level autocomplete and typo checks.