cheat sheet

satisfies Operator

TypeScript's satisfies operator checks a value against a type while preserving its narrow literal inferred type. The middle ground between an annotation that widens and an as cast that lies.

satisfies Operator — Type-check without widening

What it is

satisfies is a TypeScript operator (added in 4.9, November 2022) that checks a value against a type without changing the value's inferred type. It sits in the gap between two older tools that always disappointed in one direction: a type annotation (const x: T = ...) widens the value to T and throws away literal precision, while a type assertion (const x = ... as T) silences the compiler entirely. satisfies is the middle path — it asks the compiler to verify your value conforms to T, then walks away with the narrow inferred type so you can index into it, key off it, or use it as a generic argument.

Use satisfies when the value is the source of truth and the type is just a guardrail. Use a regular annotation when you actively want widening (typically for function parameters, return positions, and reassignable bindings). Use as only when you genuinely know better than the compiler, which is rare.

Install

Nothing to install — the operator is part of the TypeScript compiler. Requires TS 4.9 or later. All examples here assume TS 5.x.

bash
npx tsc --version

Output:

code
Version 5.5.4

Syntax

The keyword goes after an expression, like as:

typescript
const value = expression satisfies Type;

Output: (none — pure compile-time check)

satisfies may be combined with as const (which goes first):

typescript
const value = expression as const satisfies Type;

Output: (none)

The three competitors

satisfies only makes sense in contrast with the two alternatives. Here is the same object with each form so you can see what's preserved and what's lost.

typescript
type Palette = Record<string, string | [number, number, number]>;

// 1. Annotation — checks AND widens
const a: Palette = {
  primary:   "#8a5cff",
  secondary: [255, 127, 80],
};
a.primary.toUpperCase();    // string method — OK because primary is string
a.secondary[0];             // Error — Palette says string | tuple; need to narrow

// 2. Assertion — no check at all (unsafe)
const b = {
  primary: "#8a5cff",
  // typo:
  secondari: [255, 127, 80],
} as Palette;
// No error! `b.secondary` is `string | tuple | undefined` at runtime — silent bug.

// 3. satisfies — checks AND preserves narrow shape
const c = {
  primary:   "#8a5cff",
  secondary: [255, 127, 80],
} satisfies Palette;
c.primary.toUpperCase();    // OK — c.primary is "#8a5cff"
const [r, g, bl] = c.secondary; // OK — secondary is [number, number, number]

Output:

csharp
(compile-only — case (a) errors on the destructure, (b) passes wrongly, (c) is correct)

satisfies is the one that fails on the typo and still lets you treat c.secondary as a tuple.

How widening happens (and why it hurts)

A regular type annotation widens the value to match the declared type. For object literals, that means losing every literal precisely-known field.

typescript
type Config = Record<string, string | number | boolean>;

const cfg: Config = {
  host: "localhost",
  port: 3000,
  tls:  false,
};

// All three values lose their narrow types:
// cfg.host : string | number | boolean
// cfg.port : string | number | boolean
// cfg.tls  : string | number | boolean

if (cfg.tls === true) {
  // OK, but TS had to re-narrow inside the if
}

Output: (none)

With satisfies the same value keeps each property's narrow type while still being verified against Config:

typescript
const cfg2 = {
  host: "localhost",
  port: 3000,
  tls:  false,
} satisfies Config;

cfg2.host.toUpperCase(); // host is string
cfg2.port.toFixed(2);    // port is number
const flipped = !cfg2.tls; // tls is boolean
console.log(flipped);

Output:

arduino
true

Pairing with as const

as const and satisfies are complementary — as const makes everything deeply readonly and keeps every literal narrow; satisfies checks the shape against a target. Together they produce the most precise immutable type you can express.

typescript
type Method = "GET" | "POST" | "PUT" | "DELETE";

const routes = {
  list:   { method: "GET",  path: "/users"      },
  create: { method: "POST", path: "/users"      },
  remove: { method: "DELETE", path: "/users/:id" },
} as const satisfies Record<string, { method: Method; path: `/${string}` }>;

type RouteName = keyof typeof routes; // "list" | "create" | "remove"
type Routes    = typeof routes;
// Each method is the literal "GET" / "POST" / "DELETE" — not just `Method`.

function call<N extends RouteName>(name: N): void {
  const r = routes[name];
  console.log(`${r.method} ${r.path}`);
}

call("list");
call("create");

Output:

bash
GET /users
POST /users

Without as const, routes.list.method would widen to string. With as const, it's "GET". With satisfies, the compiler still rejected typos (try method: "GIT" — it errors).

Constraining keys to a union

A common case: you want the keys of an object to be exactly some union, but you also want the values themselves to keep narrow types. satisfies Record<Keys, ValueShape> makes the compiler check both — every key must be in Keys, and every value must match ValueShape — without widening the values.

typescript
type Lang = "en" | "fr" | "es";

const greetings = {
  en: "Hello",
  fr: "Bonjour",
  es: "Hola",
} satisfies Record<Lang, string>;

// Errors:
// missing language:
// const partial = { en: "Hi" } satisfies Record<Lang, string>; // Error
// typo'd key:
// const bad = { ...greetings, de: "Hallo" } satisfies Record<Lang, string>; // Error

// Values keep their narrow literals
type Hello = typeof greetings.en;   // "Hello"
type Lookup = typeof greetings[Lang]; // "Hello" | "Bonjour" | "Hola"

Output: (none — compile-time only)

Where satisfies is the wrong tool

satisfies is not a free upgrade — it's the wrong choice when you actually want the widened type.

typescript
// A function parameter: you DO want widening so callers can pass any string
function setRole(role: "admin" | "user"): void { /* ... */ }
// satisfies wouldn't make sense here — there's no value to keep narrow.

// A reassignable binding: satisfies + as const makes future assignments error
let mode = "prod" as const satisfies "prod" | "dev";
// mode = "dev"; // Error — mode is readonly "prod"

// A return-position type: you usually want the public contract widened
function defaults(): Record<string, unknown> {
  // returning `{ host: "..." } satisfies Record<string, unknown>` widens to
  // Record anyway in the public signature — satisfies here is redundant.
  return { host: "localhost" };
}

Output: (none — only the mode = "dev" line errors)

Composing satisfies with generics

satisfies interacts cleanly with generic helpers. A common pattern: define a helper that returns the input unchanged but enforces a shape, so call sites get inference and type-check.

typescript
function defineConfig<T extends Record<string, unknown>>(cfg: T): T {
  return cfg;
}

const cfg1 = defineConfig({
  port: 3000,
  ssl:  false,
});
// cfg1.port: number (narrow inferred T)

const cfg2 = {
  port: 3000,
  ssl:  false,
} satisfies Record<string, number | boolean>;
// cfg2.port: 3000 (literal!)

Output: (none)

satisfies keeps literalsdefineConfig only keeps the shape of T. If you want literal precision without a helper, as const satisfies T is the answer.

Discriminated unions from as const satisfies

Building a discriminated union by hand is one of the strongest cases for as const satisfies. The compiler checks the union exhaustively while every entry keeps its narrow kind literal.

typescript
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rect"; w: number; h: number };

const shapes = [
  { kind: "circle", radius: 5 },
  { kind: "square", side: 3 },
  { kind: "rect",   w: 10, h: 20 },
] as const satisfies readonly Shape[];

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;
    case "square": return s.side ** 2;
    case "rect":   return s.w * s.h;
  }
}

for (const s of shapes) console.log(s.kind, area(s));

Output:

arduino
circle 78.53981633974483
square 9
rect 200

A misspelt kind (e.g. "circel") is caught immediately by satisfies.

Essential options & forms

FormWhen to use
value satisfies TCheck against T, keep narrow inferred type
value as const satisfies TCombine immutability + literal narrowness with a shape check
value as TYou're certain — silence the compiler (last resort)
const x: T = valueYou want widening to T (e.g. interface contracts)
function f(x: T)Use a plain annotation for parameters; satisfies doesn't apply
return value satisfies TSometimes useful in arrow bodies where return position annotation is awkward

Common pitfalls

  1. satisfies on a let — Works, but every reassignment re-checks the literal. With as const, the binding is effectively read-only and future writes error. Use const + satisfies unless you really need reassignment.
  2. Forgetting as const when literals matter{ kind: "circle" } satisfies Shape still widens kind to string. Add as const.
  3. Excess-property checks are bypassedsatisfies does not trigger excess-property checks the way an annotation does. Extra fields on a fresh literal pass silently as long as the required ones are present. Use a strict mapped helper if you need to forbid extras.
  4. Generic inference doesn't kick invalue satisfies SomeGeneric<infer X> is illegal. infer only lives in conditional types; you can't pull a type out of satisfies.
  5. Errors point at the value, not the type — Compiler messages list which property of your value fails; if the type itself was wrong (e.g. you forgot a member), the error can look confusing. Read the message carefully.
  6. Confusing satisfies with type assertionssatisfies never casts. If you see something like (x as T) satisfies U you've usually got a logic bug.
  7. Tuples aren't preserved without as const[1, 2, 3] satisfies number[] is fine, but the result is still number[]. Use as const satisfies readonly number[] for tuple precision.
  8. satisfies with Record<string, ...> accepts any extra keys — Because Record<string, V> already permits arbitrary string keys, satisfies won't flag misspellings. Constrain the key set with a literal union (Record<"a" | "b", V>) when you care.
  9. Combining with class instancesnew MyClass() satisfies MyInterface works, but the inferred type stays MyClass, not the interface. Use it to assert a structural match without losing the concrete class type.
  10. Editor experience varies — Some older tsserver versions show the widened type on hover for satisfies even though the real inferred type is narrow. Trust the compiler over the tooltip if they disagree, and update to a recent TS.

Real-world recipes

Recipe 1 — Theme tokens with narrow literal values

You want theme tokens that the compiler validates, that downstream code can read as literals (for branded as types, CSS-in-JS keys, etc.), and that auto-complete in IDEs.

typescript
type Token = `--${string}`;

const theme = {
  "--color-primary":   "#8a5cff",
  "--color-secondary": "#ff7f50",
  "--font-mono":       "ui-monospace, monospace",
  "--radius-md":       "8px",
} as const satisfies Record<Token, string>;

type ThemeKey = keyof typeof theme;
// "--color-primary" | "--color-secondary" | "--font-mono" | "--radius-md"

function v(key: ThemeKey): string {
  return `var(${key})`;
}

console.log(v("--color-primary"));
// console.log(v("--color-tertiary")); // Error — not a ThemeKey

Output:

css
var(--color-primary)

Recipe 2 — Route table with method + path constraints

Routes need their method to be one of a finite set, paths to begin with /, and each entry to keep its literal value so the router can dispatch on it.

typescript
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Route = { method: HttpMethod; path: `/${string}` };

const routes = {
  listUsers:   { method: "GET",    path: "/users"           },
  getUser:     { method: "GET",    path: "/users/:id"       },
  createUser:  { method: "POST",   path: "/users"           },
  updateUser:  { method: "PUT",    path: "/users/:id"       },
  deleteUser:  { method: "DELETE", path: "/users/:id"       },
} as const satisfies Record<string, Route>;

type RouteName = keyof typeof routes;
type MethodOf<N extends RouteName> = typeof routes[N]["method"];

function describe<N extends RouteName>(name: N): string {
  const r = routes[name];
  return `${r.method} ${r.path}`;
}

console.log(describe("listUsers"));
console.log(describe("deleteUser"));

Output:

bash
GET /users
DELETE /users/:id

Recipe 3 — Build a typed env reader from a defaults map

Centralise defaults in one object, derive both the env's expected type and its default values from it.

typescript
const envDefaults = {
  PORT:      3000,
  HOST:      "0.0.0.0",
  LOG_LEVEL: "info",
  DEBUG:     false,
} as const satisfies Record<string, string | number | boolean>;

type EnvKey = keyof typeof envDefaults;
type EnvDefault<K extends EnvKey> = typeof envDefaults[K];

function env<K extends EnvKey>(key: K): EnvDefault<K> {
  const raw = process.env[key];
  if (raw === undefined) return envDefaults[key];
  const d = envDefaults[key];
  if (typeof d === "number")  return Number(raw) as EnvDefault<K>;
  if (typeof d === "boolean") return (raw === "true") as EnvDefault<K>;
  return raw as EnvDefault<K>;
}

const port = env("PORT");   // number (literal 3000 if no env override)
const host = env("HOST");   // string
const dbg  = env("DEBUG");  // boolean
console.log(`server on ${host}:${port} (debug=${dbg})`);

Output:

ini
server on 0.0.0.0:3000 (debug=false)

Recipe 4 — JSON config validated against a schema

Loading a JSON file and validating it at compile time when the literal is embedded. satisfies catches typos and missing keys without erasing the literal values from the rest of the file.

typescript
type FeatureFlags = {
  search:    boolean;
  comments:  boolean;
  betaFeatures: readonly string[];
};

const flags = {
  search:    true,
  comments:  false,
  betaFeatures: ["dark-mode", "new-editor"],
} as const satisfies FeatureFlags;

// Narrow literal access still works
if (flags.search) console.log("search is on");

// Iteration sees a readonly tuple of two literal strings
for (const feat of flags.betaFeatures) {
  console.log("beta:", feat);
}

Output:

vbnet
search is on
beta: dark-mode
beta: new-editor

Recipe 5 — A function-keyed dispatcher

You have a map from event names to handler functions, you want each handler's parameter type to be inferred from how the dispatcher calls it. satisfies keeps each function signature narrow.

typescript
type EventHandlers = Record<string, (payload: any) => void>;

const handlers = {
  click:    (p: { x: number; y: number }) => console.log("click", p.x, p.y),
  keypress: (p: { key: string })          => console.log("key", p.key),
  resize:   (p: { w: number; h: number }) => console.log("size", p.w, "x", p.h),
} satisfies EventHandlers;

type Handler<K extends keyof typeof handlers> = typeof handlers[K];
type PayloadOf<K extends keyof typeof handlers> = Parameters<Handler<K>>[0];

function dispatch<K extends keyof typeof handlers>(event: K, payload: PayloadOf<K>): void {
  handlers[event](payload);
}

dispatch("click",    { x: 1, y: 2 });
dispatch("keypress", { key: "Enter" });
// dispatch("click", { key: "Enter" }); // Error — wrong payload shape

Output:

arduino
click 1 2
key Enter

Recipe 6 — Constraining a const that powers typeof

When you derive types from a value with typeof, you usually want as const satisfies Tas const to keep literals, satisfies to make sure the value matches your contract.

typescript
type Tier = { name: string; price: number; features: readonly string[] };

const plans = [
  { name: "Free",  price: 0,  features: ["1 user"] },
  { name: "Pro",   price: 19, features: ["10 users", "API"] },
  { name: "Team",  price: 49, features: ["unlimited", "API", "SSO"] },
] as const satisfies readonly Tier[];

type PlanName = typeof plans[number]["name"];
// "Free" | "Pro" | "Team"

function priceOf(name: PlanName): number {
  return plans.find((p) => p.name === name)!.price;
}

console.log(priceOf("Pro"));

Output:

code
19

Recipe 7 — Strict literal enums with exhaustive switches

Avoid enum entirely: use as const satisfies to build a constant object that doubles as a type union, and pair it with an exhaustive switch using never.

typescript
const STATUS = {
  PENDING:  "pending",
  ACTIVE:   "active",
  INACTIVE: "inactive",
} as const satisfies Record<string, string>;

type StatusKey   = keyof typeof STATUS;          // "PENDING" | "ACTIVE" | "INACTIVE"
type StatusValue = typeof STATUS[StatusKey];     // "pending" | "active" | "inactive"

function label(s: StatusValue): string {
  switch (s) {
    case "pending":  return "Waiting…";
    case "active":   return "Running";
    case "inactive": return "Disabled";
    default: {
      const _exhaustive: never = s;
      return _exhaustive;
    }
  }
}

console.log(label(STATUS.ACTIVE));
console.log(label(STATUS.PENDING));

Output:

sql
Running
Waiting…

Recipe 8 — Validate an object shape that has both required and free-form parts

satisfies shines when half of an object is structurally known and half is a flexible record. Combine an intersection type with satisfies to keep both halves narrow.

typescript
type Meta = {
  name:    string;
  version: `${number}.${number}.${number}`;
};

const pkg = {
  name:    "jockey",
  version: "1.4.0",
  // free-form extras
  homepage: "https://example.com",
  keywords: ["cheatsheets", "astro"],
} satisfies Meta & Record<string, unknown>;

const major = pkg.version.split(".")[0];
console.log(`${pkg.name}@${pkg.version} (major ${major})`);
console.log("tags:", pkg.keywords.join(", "));

Output:

css
jockey@1.4.0 (major 1)
tags: cheatsheets, astro