satisfies, as const and const type parameters

TypeScript · Advanced · 12 min

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 error

It'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 T checks against T but keeps the inferred type
  • as const infers readonly literal types and tuples
  • <const T> gives callers as const-style inference automatically
  • as T silences checks; satisfies T performs them

Watch out for

  • Using an annotation on lookup tables and then losing key-level autocomplete and typo checks.

Check yourself

  1. const c = { port: 80 } satisfies { port: number }. What's the type of c.port?
  2. Which line produces a compile error?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html