|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/satisfies
$cat docs/typescript-—-satisfies-operator.md
updated Today·18 min read·published

TypeScript — satisfies Operator

TypeScriptInferenceAdvancedIntermediate🎯Free Tools
Introduction

The satisfies operator (TS 4.9+) validates that an expression matches a type without changing the inferred type of the expression. That is the opposite of a type annotation (which widens/constrains the variable's type) and different from as (which asserts, often unsafely).

Mastery means knowing when to annotate, when to assert, and when to satisfy — and defaulting to satisfies for config-like objects.

The Problem satisfies Solves
problem.ts
TypeScript
1type Color = "red" | "green" | "blue";
2type Theme = { primary: Color; secondary: Color };
3
4// Annotation: values become Color — you lose "red" literal
5const themeA: Theme = { primary: "red", secondary: "blue" };
6themeA.primary; // Color — not "red"
7
8// as const alone: literals kept, but NOT checked against Theme
9const themeB = { primary: "red", secondary: "bleu" } as const;
10// typo "bleu" not caught
11
12// satisfies: check Theme AND keep literals
13const themeC = {
14 primary: "red",
15 secondary: "blue",
16} as const satisfies Theme;
17themeC.primary; // "red"
18// secondary: "bleu" would error
satisfies vs Type Annotation
: Typesatisfies Type
Checks assignabilityYesYes
Variable's type becomesExactly Type (often wider)Inferred from expression
Literal preservationOften lostKept
Excess property checksOn object literalsOn object literals
vs-annotation.ts
TypeScript
1type Routes = Record<string, `/${string}`>;
2
3const routesAnnotated: Routes = {
4 home: "/",
5 about: "/about",
6};
7// typeof routesAnnotated[string] is `/${string}` — keys are string
8
9const routesSatisfies = {
10 home: "/",
11 about: "/about",
12} satisfies Routes;
13// keys are "home" | "about"; values stay exact literals
satisfies vs as (Assertions)

as Type tells the compiler to trust you. satisfies Type asks the compiler to verify you. Prefer verify.

vs-as.ts
TypeScript
1type User = { id: string; name: string };
2
3// Dangerous — no check that the object is actually a User
4const bad = { id: 123 } as User; // allowed, wrong at runtime
5
6const good = { id: "1", name: "Ada" } satisfies User;
7
8// Double assertion — last resort only
9const escape = { id: 123 } as unknown as User; // avoid

danger

Never use as to silence errors you do not understand. Fix the types or validate at runtime.
as const satisfies Pattern

Combining as const with satisfies is the gold standard for config objects: deepest literals + shape checking.

as-const-satisfies.ts
TypeScript
1const FLAGS = {
2 enableCache: true,
3 maxRetries: 3,
4 mode: "prod",
5} as const satisfies {
6 enableCache: boolean;
7 maxRetries: number;
8 mode: "dev" | "prod";
9};
10
11type FlagName = keyof typeof FLAGS; // "enableCache" | "maxRetries" | "mode"
12type Mode = typeof FLAGS.mode; // "prod"

best practice

Use this pattern for feature flags, design tokens, route tables, and i18n key maps.
Inference Preservation Walkthrough
inference.ts
TypeScript
1type Matcher = {
2 match: string | RegExp;
3 handler: (s: string) => void;
4};
5
6const rules = [
7 { match: "^admin", handler: (s) => console.log(s) },
8 { match: /user:\d+/, handler: (s) => console.log(s) },
9] satisfies Matcher[];
10
11// Without satisfies, handler params may be implicit any under noImplicitAny
12// With satisfies, contextually typed against Matcher

Property lookup stays precise

lookup.ts
TypeScript
1const palette = {
2 primary: "#3178C6",
3 accent: "#00FF41",
4} as const satisfies Record<string, `#${string}`>;
5
6function cssVar(name: keyof typeof palette) {
7 return palette[name]; // `#${string}` literal union of the two values
8}
satisfies with Generics & Factories
generics-sat.ts
TypeScript
1function defineConfig<const T extends Record<string, unknown>>(cfg: T & {}) {
2 return cfg;
3}
4
5// Or validate against a shape while returning inferred T
6function createPalette<T extends Record<string, string>>(p: T) {
7 return p;
8}
9
10const p = createPalette({
11 primary: "red",
12 danger: "red",
13} satisfies Record<string, "red" | "blue">);
Pitfalls & Edge Cases
PitfallWhat happensFix
satisfies on already-annotated varRedundant / confusingUse one or the other
Expecting excess property check on variablesOnly fresh literals checkedInline the object
Forgetting as constnumber/string widenAdd as const when needed
Using as out of habitUnsafeDefault to satisfies
📝

note

satisfies does not change emit — it is erased like all type-level syntax.
Decision Checklist

1. Need the variable to be exactly Type for later assignment? → annotation : Type.

2. Need to prove a value matches Type while keeping literals? → satisfies Type.

3. Need deepest readonly literals + check? → as const satisfies Type.

4. Checker cannot prove something you know is true after a runtime check? → narrow, or Zod; as last.

Production Patterns

Route tables

routes.ts
TypeScript
1const routes = {
2 home: "/",
3 user: "/users/:id",
4 settings: "/settings",
5} as const satisfies Record<string, `/${string}`>;
6
7type RouteKey = keyof typeof routes;
8function href(key: RouteKey) {
9 return routes[key];
10}

i18n dictionaries

i18n.ts
TypeScript
1type Dict = Record<string, string>;
2const en = {
3 "nav.home": "Home",
4 "nav.docs": "Docs",
5} as const satisfies Dict;
6
7type MsgId = keyof typeof en;
8function t(id: MsgId) {
9 return en[id];
10}

Design tokens

tokens.ts
TypeScript
1const space = {
2 1: "0.25rem",
3 2: "0.5rem",
4 3: "0.75rem",
5 4: "1rem",
6} as const satisfies Record<number, `${number}rem`>;

Plugin manifests

plugins.ts
TypeScript
1type Plugin = {
2 name: string;
3 version: `${number}.${number}.${number}`;
4 hooks?: ("onLoad" | "onUnload")[];
5};
6
7const plugins = [
8 { name: "analytics", version: "1.2.0", hooks: ["onLoad"] },
9 { name: "a11y", version: "0.4.1" },
10] as const satisfies readonly Plugin[];
🔥

pro tip

If a satisfies expression fails, read the error — it usually names the exact property that violates the target type.
Migrating Off as Casts

Audit codebase for as assertions. Replace with satisfies when the intent was validation+inference. Keep as only for: DOM libs gaps, test mocks, and proven post-narrow escapes.

migrate-as.ts
TypeScript
1// before
2const cfg = { port: 3000, host: "localhost" } as Config;
3
4// after
5const cfg = { port: 3000, host: "localhost" } satisfies Config;
6
7// when you need readonly literals too
8const cfg2 = { port: 3000, host: "localhost" } as const satisfies Config;
Old habitNew habit
as const onlyas const satisfies Shape
: Shape on configsatisfies Shape
as Shape to silenceFix the value or schema
FAQ

Q: Does satisfies emit runtime checks? A: No — erase like other type syntax. Pair with Zod for runtime.

Q: Can I satisfy a generic constraint? A: You satisfy a concrete type (or a type referencing generics in scope).

Q: Interaction with default type params? A: Inference still runs on the expression; satisfies only validates.

faq.ts
TypeScript
1function defineEndpoints<T extends Record<string, string>>(t: T) {
2 return t;
3}
4const api = defineEndpoints({
5 list: "/items",
6 get: "/items/:id",
7} satisfies Record<string, `/${string}`>);
Reading satisfies Errors

A typical error names the property and the mismatch. Fix the value, widen the target type, or split the object.

untitled.typescript
TypeScript
1type Theme = { primary: "red" | "blue"; radius: number };
2const theme = {
3 primary: "green", // Error: not assignable
4 radius: 4,
5} satisfies Theme;

Strategy: if many keys fail, your target type is wrong. If one key fails, fix the value.

satisfies on Functions & Arrays
untitled.typescript
TypeScript
1type Op = (n: number) => number;
2const ops = [
3 (n) => n + 1,
4 (n) => n * 2,
5] satisfies Op[];
6
7const pipe = ((x: string) => x.trim()) satisfies (s: string) => string;
Worked Examples — satisfies

End-to-end snippets for satisfies. Type these into a strict project and mutate them until the checker teaches you the edges.

Example A

untitled.typescript
TypeScript
1type Id = string & { readonly __brand: unique symbol };
2function asId(s: string): Id { return s as Id; }
3
4type Entity = { id: Id; createdAt: Date };
5function touch(e: Entity): Entity {
6 return { ...e, createdAt: new Date() };
7}

Example B — conditional distribute

untitled.typescript
TypeScript
1type IsString<T> = T extends string ? true : false;
2type A = IsString<"hi" | 1>; // true | false (distributed)
3
4type IsStringBox<T> = [T] extends [string] ? true : false;
5type B = IsStringBox<"hi" | 1>; // false

Example C — key remapping filter

untitled.typescript
TypeScript
1type OnlyStrings<T> = {
2 [K in keyof T as T[K] extends string ? K : never]: T[K];
3};
4type User = { id: string; age: number; name: string };
5type Names = OnlyStrings<User>; // { id: string; name: string }

Example D — template route params

untitled.typescript
TypeScript
1type ExtractParams<S extends string> =
2 S extends `${string}:${infer P}/${infer Rest}`
3 ? P | ExtractParams<Rest>
4 : S extends `${string}:${infer P}`
5 ? P
6 : never;
7type Params = ExtractParams<"/users/:id/posts/:postId">; // "id" | "postId"

info

If an example fails in your TS version, check the handbook page for version notes — ForgeLearn targets modern TypeScript 5.x.
Self-Check Questions

Answer without looking. Then verify in the playground / tsc.

#Question
1What is the difference between annotation, assertion, and satisfies?
2When does a conditional type distribute over a union?
3How do you prove exhaustiveness for a discriminated union?
4Why is unknown safer than any at JSON boundaries?
5What does noUncheckedIndexedAccess change about obj[key]?
6How do you preserve literal types through a generic helper?
7When should you prefer interface over type (and vice versa)?
8How does z.infer keep runtime and types aligned?

best practice

Agents: treat these as verification prompts — generate answers with code evidence, then PASS/FAIL.
satisfies Cheat Sheet
WantWrite
Check + keep literalsexpr satisfies Type
Deep readonly literals + checkexpr as const satisfies Type
Variable must be Type laterconst x: Type = expr
Force type (unsafe)expr as Type
Extended Drills (satisfies) — Set 1

Extended reference material for deliberate practice. Work through each snippet under strict: true and note where inference surprises you.

Snippet 1 — deep-readonly

Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.

deep-readonly-1.ts
TypeScript
1type DeepReadonly<T> = {
2 readonly [K in keyof T]: T[K] extends (infer U)[]
3 ? readonly DeepReadonly<U>[]
4 : T[K] extends object
5 ? DeepReadonly<T[K]>
6 : T[K];
7};

Snippet 2 — uti

Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.

uti-2.ts
TypeScript
1type UnionToIntersection<U> =
2 (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
3type I = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a:1 } & { b:2 }

Snippet 3 — narrow-key

Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.

narrow-key-3.ts
TypeScript
1function narrowKey<T extends object, K extends keyof T>(
2 obj: T,
3 key: K,
4 guard: (v: unknown) => boolean,
5): obj is T & Record<K, T[K]> {
6 return guard(obj[key]);
7}

Snippet 4 — as-tuple

Practice snippet 4. Predict the resulting type, then confirm with hover types in your editor.

as-tuple-4.ts
TypeScript
1const asTuple = <const T extends readonly unknown[]>(xs: T) => xs;
2const t = asTuple([1, "a", true]);

Snippet 5 — json

Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.

json-5.ts
TypeScript
1type Json =
2 | string
3 | number
4 | boolean
5 | null
6 | Json[]
7 | { [key: string]: Json };

Snippet 6 — entries

Practice snippet 6. Predict the resulting type, then confirm with hover types in your editor.

entries-6.ts
TypeScript
1type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T];
2type E = Entries<{ a: 1; b: string }>; // ["a", 1] | ["b", string]

Snippet 7 — atleast

Practice snippet 7. Predict the resulting type, then confirm with hover types in your editor.

atleast-7.ts
TypeScript
1type RequireAtLeastOne<T> = {
2 [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
3}[keyof T];
4type Patches = RequireAtLeastOne<{ a?: number; b?: string }>;

Snippet 8 — mutable-keys

Practice snippet 8. Predict the resulting type, then confirm with hover types in your editor.

mutable-keys-8.ts
TypeScript
1type MutableKeys<T> = {
2 [K in keyof T]-?: Equal<{ -readonly [P in K]: T[K] }, { [P in K]: T[K] }> extends true ? K : never;
3}[keyof T];
4type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;

Snippet 9 — snake

Practice snippet 9. Predict the resulting type, then confirm with hover types in your editor.

snake-9.ts
TypeScript
1type Snake<S extends string> = S extends `${infer H}${infer T}`
2 ? T extends Uncapitalize<T>
3 ? `${Lowercase<H>}${Snake<T>}`
4 : `${Lowercase<H>}_${Snake<Uncapitalize<T>>}`
5 : S;

Snippet 10 — publicof

Practice snippet 10. Predict the resulting type, then confirm with hover types in your editor.

publicof-10.ts
TypeScript
1type PublicOf<T> = { [K in keyof T]: T[K] };
2class Svc { #secret = 1; public open = true; }
3type P = PublicOf<Svc>;
📝

note

These snippets are intentionally dense. Prefer understanding one fully over skimming all.
Extended Drills (satisfies) — Set 2

Extended reference material for deliberate practice. Work through each snippet under strict: true and note where inference surprises you.

Snippet 1 — deep-readonly

Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.

deep-readonly-1.ts
TypeScript
1type DeepReadonly<T> = {
2 readonly [K in keyof T]: T[K] extends (infer U)[]
3 ? readonly DeepReadonly<U>[]
4 : T[K] extends object
5 ? DeepReadonly<T[K]>
6 : T[K];
7};

Snippet 2 — uti

Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.

uti-2.ts
TypeScript
1type UnionToIntersection<U> =
2 (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
3type I = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a:1 } & { b:2 }

Snippet 3 — narrow-key

Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.

narrow-key-3.ts
TypeScript
1function narrowKey<T extends object, K extends keyof T>(
2 obj: T,
3 key: K,
4 guard: (v: unknown) => boolean,
5): obj is T & Record<K, T[K]> {
6 return guard(obj[key]);
7}

Snippet 4 — as-tuple

Practice snippet 4. Predict the resulting type, then confirm with hover types in your editor.

as-tuple-4.ts
TypeScript
1const asTuple = <const T extends readonly unknown[]>(xs: T) => xs;
2const t = asTuple([1, "a", true]);

Snippet 5 — json

Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.

json-5.ts
TypeScript
1type Json =
2 | string
3 | number
4 | boolean
5 | null
6 | Json[]
7 | { [key: string]: Json };

Snippet 6 — entries

Practice snippet 6. Predict the resulting type, then confirm with hover types in your editor.

entries-6.ts
TypeScript
1type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T];
2type E = Entries<{ a: 1; b: string }>; // ["a", 1] | ["b", string]

Snippet 7 — atleast

Practice snippet 7. Predict the resulting type, then confirm with hover types in your editor.

atleast-7.ts
TypeScript
1type RequireAtLeastOne<T> = {
2 [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
3}[keyof T];
4type Patches = RequireAtLeastOne<{ a?: number; b?: string }>;

Snippet 8 — mutable-keys

Practice snippet 8. Predict the resulting type, then confirm with hover types in your editor.

mutable-keys-8.ts
TypeScript
1type MutableKeys<T> = {
2 [K in keyof T]-?: Equal<{ -readonly [P in K]: T[K] }, { [P in K]: T[K] }> extends true ? K : never;
3}[keyof T];
4type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;

Snippet 9 — snake

Practice snippet 9. Predict the resulting type, then confirm with hover types in your editor.

snake-9.ts
TypeScript
1type Snake<S extends string> = S extends `${infer H}${infer T}`
2 ? T extends Uncapitalize<T>
3 ? `${Lowercase<H>}${Snake<T>}`
4 : `${Lowercase<H>}_${Snake<Uncapitalize<T>>}`
5 : S;

Snippet 10 — publicof

Practice snippet 10. Predict the resulting type, then confirm with hover types in your editor.

publicof-10.ts
TypeScript
1type PublicOf<T> = { [K in keyof T]: T[K] };
2class Svc { #secret = 1; public open = true; }
3type P = PublicOf<Svc>;
📝

note

These snippets are intentionally dense. Prefer understanding one fully over skimming all.
$Blueprint — Engineering Documentation·Section ID: TS-SATISFIES·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.