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.
npx tsc --version
Output:
Version 5.5.4
Syntax
The keyword goes after an expression, like as:
const value = expression satisfies Type;
Output: (none — pure compile-time check)
satisfies may be combined with as const (which goes first):
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.
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:
(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.
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:
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:
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.
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:
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.
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.
// 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.
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 literals — defineConfig 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.
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:
circle 78.53981633974483
square 9
rect 200
A misspelt kind (e.g. "circel") is caught immediately by satisfies.
Essential options & forms
| Form | When to use |
|---|---|
value satisfies T | Check against T, keep narrow inferred type |
value as const satisfies T | Combine immutability + literal narrowness with a shape check |
value as T | You're certain — silence the compiler (last resort) |
const x: T = value | You 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 T | Sometimes useful in arrow bodies where return position annotation is awkward |
Common pitfalls
satisfieson alet— Works, but every reassignment re-checks the literal. Withas const, the binding is effectively read-only and future writes error. Useconst+satisfiesunless you really need reassignment.- Forgetting
as constwhen literals matter —{ kind: "circle" } satisfies Shapestill widenskindtostring. Addas const. - Excess-property checks are bypassed —
satisfiesdoes 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. - Generic inference doesn't kick in —
value satisfies SomeGeneric<infer X>is illegal.inferonly lives in conditional types; you can't pull a type out ofsatisfies. - 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.
- Confusing
satisfieswith type assertions —satisfiesnever casts. If you see something like(x as T) satisfies Uyou've usually got a logic bug. - Tuples aren't preserved without
as const—[1, 2, 3] satisfies number[]is fine, but the result is stillnumber[]. Useas const satisfies readonly number[]for tuple precision. satisfieswithRecord<string, ...>accepts any extra keys — BecauseRecord<string, V>already permits arbitrary string keys,satisfieswon't flag misspellings. Constrain the key set with a literal union (Record<"a" | "b", V>) when you care.- Combining with class instances —
new MyClass() satisfies MyInterfaceworks, but the inferred type staysMyClass, not the interface. Use it to assert a structural match without losing the concrete class type. - Editor experience varies — Some older
tsserverversions show the widened type on hover forsatisfieseven 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.
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:
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.
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:
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.
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:
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.
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:
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.
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:
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 T — as const to keep literals, satisfies to make sure the value matches your contract.
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:
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.
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:
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.
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:
jockey@1.4.0 (major 1)
tags: cheatsheets, astro