TypeScript satisfies: Validate Config Without Losing Types

Why satisfies beats type annotations and as casts for TypeScript config objects: shape checks, exact keys, and narrowed types kept intact.

TypeScript satisfies: Validate Config Without Losing Types
In this essay

For constant objects such as route maps, feature flags, and environment defaults, TypeScript's satisfies operator is usually a better choice than a type annotation or an as cast. It checks the value against a shape you define, but keeps the narrower type TypeScript inferred, so exact keys and specific value types stay available to the rest of your code.

The operator arrived in TypeScript 4.9, released in November 2022. It is mature enough for most codebases, yet many projects still annotate config objects out of habit. Here is why that habit costs more than it looks.

Three ways to type a constant

Take a small route table. The goal is simple: every entry must have a path and an auth flag, and a typo anywhere should fail the build.

The usual first attempt is an annotation:

type Route = { path: string; auth: boolean };

const routes: Record<string, Route> = {
  home: { path: "/", auth: false },
  dashboard: { path: "/dashboard", auth: true },
};

routes.dashbord.path; // compiles: any string key is allowed

The annotation does validate each entry. The problem is that the declared type replaces the inferred one. TypeScript now believes routes accepts any string key, so the misspelled dashbord compiles and fails at runtime (with noUncheckedIndexedAccess on, you instead get an "possibly undefined" warning on every key, typo or not). You also lose keyof typeof routes, which would otherwise give you a precise union of route names.

The second attempt is a cast:

const fallback = { path: "/" } as Route; // compiles, auth is missing

A type assertion tells the compiler to trust you. The TypeScript handbook explains that assertions are allowed when converting to a more specific or less specific version of a type; only "impossible" conversions are rejected. Missing properties slip through, which is the opposite of what a config check should do.

The third option is satisfies:

const routes = {
  home: { path: "/", auth: false },
  dashboard: { path: "/dashboard", auth: true },
} satisfies Record<string, Route>;

routes.dashbord; // error: property does not exist
type RouteName = keyof typeof routes; // "home" | "dashboard"

Every entry is still validated against Route. But the variable keeps its inferred type, so the typo fails and RouteName becomes an exact union you can reuse for typed links or navigation helpers.

What satisfies actually checks

A steel go/no-go gauge beside a precision-machined aluminum part on raw concrete, illustrating a fit check that leaves the part unchanged

satisfies asks one question: is this expression assignable to the given type? If not, you get an error right at the expression. If so, the expression's own type flows on unchanged.

Two details make it more useful than it sounds.

First, object literals still get excess property checks. The TypeScript 4.9 release notes use a color palette typed as Record<Colors, ...> where a misspelled key like bleu is caught. A typo in a key is reported, not silently accepted.

Second, union-typed values keep their narrowed member. Consider environment defaults:

type Env = Record<"API_URL" | "TIMEOUT_MS", string | number>;

const env = {
  API_URL: "https://api.example.com",
  TIMEOUT_MS: 5000,
} satisfies Env;

env.TIMEOUT_MS.toFixed(0); // ok: inferred as number

With an annotation, TIMEOUT_MS would be string | number, and you would need a type check before calling toFixed. With satisfies, the compiler remembers it is a number. Remove API_URL and the build fails, because the Record demands both keys.

Keeping literals with as const satisfies

Labeled-free wooden type drawer with neatly sorted compartments, a metaphor for exact keys kept in a typed configuration object

Sometimes you need the literal values themselves, for example to derive a union of allowed HTTP methods. Combining as const with satisfies does that:

const methods = ["GET", "POST"] as const satisfies readonly string[];
type Method = (typeof methods)[number]; // "GET" | "POST"

Note the readonly in the target type. as const produces a readonly tuple, and a readonly array is not assignable to a mutable string[]. If a confusing error appears right after you add as const, check whether the target type needs readonly.

Where satisfies is the wrong tool

It is not a universal replacement for annotations.

  • Public module boundaries. When you export a value for other teams, its inferred type becomes part of your API. If you want consumers to depend on a stable, wider contract, an explicit annotation is clearer and lets you change internals without breaking them.
  • Function signatures. satisfies applies to expressions. Parameters and return types still need ordinary annotations.
  • Runtime data. satisfies is erased at compile time. It cannot validate JSON from an API, a database row, or process.env. Use a runtime validator such as Zod, or hand-written checks, for anything that crosses a process boundary.

That last point matters most. satisfies guards values you write in source code. It says nothing about values that arrive while the program runs.

A practical rule for config files

A simple heuristic works well for most teams:

  1. For constant objects defined in your own code, such as routes, flags, theme tokens, and lookup tables, use satisfies.
  2. Add as const when you need the literal values, and make the target type readonly.
  3. Annotate exported values when you want a deliberately wider public contract.
  4. Validate external input at runtime, never with a compile-time operator alone.

The pay-off is concrete: key typos fail in the editor, narrowed types remove defensive checks, and unions like RouteName come for free from data you already wrote. The cost is one keyword placed after the object instead of a type placed before the variable name.

Sources

  1. nvd.nist.gov
  2. mallory.ai
  3. slcyber.io
  4. nextjs_backend
  5. assetnote.io
  6. typescriptlang.org

AI-assisted · Reviewed by PKN

0

Responses

Loading comments…