TypeScript — satisfies Operator
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.
| 1 | type Color = "red" | "green" | "blue"; |
| 2 | type Theme = { primary: Color; secondary: Color }; |
| 3 | |
| 4 | // Annotation: values become Color — you lose "red" literal |
| 5 | const themeA: Theme = { primary: "red", secondary: "blue" }; |
| 6 | themeA.primary; // Color — not "red" |
| 7 | |
| 8 | // as const alone: literals kept, but NOT checked against Theme |
| 9 | const themeB = { primary: "red", secondary: "bleu" } as const; |
| 10 | // typo "bleu" not caught |
| 11 | |
| 12 | // satisfies: check Theme AND keep literals |
| 13 | const themeC = { |
| 14 | primary: "red", |
| 15 | secondary: "blue", |
| 16 | } as const satisfies Theme; |
| 17 | themeC.primary; // "red" |
| 18 | // secondary: "bleu" would error |
| : Type | satisfies Type | |
|---|---|---|
| Checks assignability | Yes | Yes |
| Variable's type becomes | Exactly Type (often wider) | Inferred from expression |
| Literal preservation | Often lost | Kept |
| Excess property checks | On object literals | On object literals |
| 1 | type Routes = Record<string, `/${string}`>; |
| 2 | |
| 3 | const routesAnnotated: Routes = { |
| 4 | home: "/", |
| 5 | about: "/about", |
| 6 | }; |
| 7 | // typeof routesAnnotated[string] is `/${string}` — keys are string |
| 8 | |
| 9 | const routesSatisfies = { |
| 10 | home: "/", |
| 11 | about: "/about", |
| 12 | } satisfies Routes; |
| 13 | // keys are "home" | "about"; values stay exact literals |
as Type tells the compiler to trust you. satisfies Type asks the compiler to verify you. Prefer verify.
| 1 | type User = { id: string; name: string }; |
| 2 | |
| 3 | // Dangerous — no check that the object is actually a User |
| 4 | const bad = { id: 123 } as User; // allowed, wrong at runtime |
| 5 | |
| 6 | const good = { id: "1", name: "Ada" } satisfies User; |
| 7 | |
| 8 | // Double assertion — last resort only |
| 9 | const escape = { id: 123 } as unknown as User; // avoid |
danger
Combining as const with satisfies is the gold standard for config objects: deepest literals + shape checking.
| 1 | const 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 | |
| 11 | type FlagName = keyof typeof FLAGS; // "enableCache" | "maxRetries" | "mode" |
| 12 | type Mode = typeof FLAGS.mode; // "prod" |
best practice
| 1 | type Matcher = { |
| 2 | match: string | RegExp; |
| 3 | handler: (s: string) => void; |
| 4 | }; |
| 5 | |
| 6 | const 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
| 1 | const palette = { |
| 2 | primary: "#3178C6", |
| 3 | accent: "#00FF41", |
| 4 | } as const satisfies Record<string, `#${string}`>; |
| 5 | |
| 6 | function cssVar(name: keyof typeof palette) { |
| 7 | return palette[name]; // `#${string}` literal union of the two values |
| 8 | } |
| 1 | function defineConfig<const T extends Record<string, unknown>>(cfg: T & {}) { |
| 2 | return cfg; |
| 3 | } |
| 4 | |
| 5 | // Or validate against a shape while returning inferred T |
| 6 | function createPalette<T extends Record<string, string>>(p: T) { |
| 7 | return p; |
| 8 | } |
| 9 | |
| 10 | const p = createPalette({ |
| 11 | primary: "red", |
| 12 | danger: "red", |
| 13 | } satisfies Record<string, "red" | "blue">); |
| Pitfall | What happens | Fix |
|---|---|---|
| satisfies on already-annotated var | Redundant / confusing | Use one or the other |
| Expecting excess property check on variables | Only fresh literals checked | Inline the object |
| Forgetting as const | number/string widen | Add as const when needed |
| Using as out of habit | Unsafe | Default to satisfies |
note
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.
Route tables
| 1 | const routes = { |
| 2 | home: "/", |
| 3 | user: "/users/:id", |
| 4 | settings: "/settings", |
| 5 | } as const satisfies Record<string, `/${string}`>; |
| 6 | |
| 7 | type RouteKey = keyof typeof routes; |
| 8 | function href(key: RouteKey) { |
| 9 | return routes[key]; |
| 10 | } |
i18n dictionaries
| 1 | type Dict = Record<string, string>; |
| 2 | const en = { |
| 3 | "nav.home": "Home", |
| 4 | "nav.docs": "Docs", |
| 5 | } as const satisfies Dict; |
| 6 | |
| 7 | type MsgId = keyof typeof en; |
| 8 | function t(id: MsgId) { |
| 9 | return en[id]; |
| 10 | } |
Design tokens
| 1 | const 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
| 1 | type Plugin = { |
| 2 | name: string; |
| 3 | version: `${number}.${number}.${number}`; |
| 4 | hooks?: ("onLoad" | "onUnload")[]; |
| 5 | }; |
| 6 | |
| 7 | const 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
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.
| 1 | // before |
| 2 | const cfg = { port: 3000, host: "localhost" } as Config; |
| 3 | |
| 4 | // after |
| 5 | const cfg = { port: 3000, host: "localhost" } satisfies Config; |
| 6 | |
| 7 | // when you need readonly literals too |
| 8 | const cfg2 = { port: 3000, host: "localhost" } as const satisfies Config; |
| Old habit | New habit |
|---|---|
| as const only | as const satisfies Shape |
| : Shape on config | satisfies Shape |
| as Shape to silence | Fix the value or schema |
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.
| 1 | function defineEndpoints<T extends Record<string, string>>(t: T) { |
| 2 | return t; |
| 3 | } |
| 4 | const api = defineEndpoints({ |
| 5 | list: "/items", |
| 6 | get: "/items/:id", |
| 7 | } satisfies Record<string, `/${string}`>); |
A typical error names the property and the mismatch. Fix the value, widen the target type, or split the object.
| 1 | type Theme = { primary: "red" | "blue"; radius: number }; |
| 2 | const 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.
| 1 | type Op = (n: number) => number; |
| 2 | const ops = [ |
| 3 | (n) => n + 1, |
| 4 | (n) => n * 2, |
| 5 | ] satisfies Op[]; |
| 6 | |
| 7 | const pipe = ((x: string) => x.trim()) satisfies (s: string) => string; |
End-to-end snippets for satisfies. Type these into a strict project and mutate them until the checker teaches you the edges.
Example A
| 1 | type Id = string & { readonly __brand: unique symbol }; |
| 2 | function asId(s: string): Id { return s as Id; } |
| 3 | |
| 4 | type Entity = { id: Id; createdAt: Date }; |
| 5 | function touch(e: Entity): Entity { |
| 6 | return { ...e, createdAt: new Date() }; |
| 7 | } |
Example B — conditional distribute
| 1 | type IsString<T> = T extends string ? true : false; |
| 2 | type A = IsString<"hi" | 1>; // true | false (distributed) |
| 3 | |
| 4 | type IsStringBox<T> = [T] extends [string] ? true : false; |
| 5 | type B = IsStringBox<"hi" | 1>; // false |
Example C — key remapping filter
| 1 | type OnlyStrings<T> = { |
| 2 | [K in keyof T as T[K] extends string ? K : never]: T[K]; |
| 3 | }; |
| 4 | type User = { id: string; age: number; name: string }; |
| 5 | type Names = OnlyStrings<User>; // { id: string; name: string } |
Example D — template route params
| 1 | type 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; |
| 7 | type Params = ExtractParams<"/users/:id/posts/:postId">; // "id" | "postId" |
info
Answer without looking. Then verify in the playground / tsc.
| # | Question |
|---|---|
| 1 | What is the difference between annotation, assertion, and satisfies? |
| 2 | When does a conditional type distribute over a union? |
| 3 | How do you prove exhaustiveness for a discriminated union? |
| 4 | Why is unknown safer than any at JSON boundaries? |
| 5 | What does noUncheckedIndexedAccess change about obj[key]? |
| 6 | How do you preserve literal types through a generic helper? |
| 7 | When should you prefer interface over type (and vice versa)? |
| 8 | How does z.infer keep runtime and types aligned? |
best practice
| Want | Write |
|---|---|
| Check + keep literals | expr satisfies Type |
| Deep readonly literals + check | expr as const satisfies Type |
| Variable must be Type later | const x: Type = expr |
| Force type (unsafe) | expr as Type |
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.
| 1 | type 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.
| 1 | type UnionToIntersection<U> = |
| 2 | (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; |
| 3 | type 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.
| 1 | function 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.
| 1 | const asTuple = <const T extends readonly unknown[]>(xs: T) => xs; |
| 2 | const t = asTuple([1, "a", true]); |
Snippet 5 — json
Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type 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.
| 1 | type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T]; |
| 2 | type 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.
| 1 | type RequireAtLeastOne<T> = { |
| 2 | [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>; |
| 3 | }[keyof T]; |
| 4 | type 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.
| 1 | type 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]; |
| 4 | type 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.
| 1 | type 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.
| 1 | type PublicOf<T> = { [K in keyof T]: T[K] }; |
| 2 | class Svc { #secret = 1; public open = true; } |
| 3 | type P = PublicOf<Svc>; |
note
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.
| 1 | type 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.
| 1 | type UnionToIntersection<U> = |
| 2 | (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; |
| 3 | type 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.
| 1 | function 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.
| 1 | const asTuple = <const T extends readonly unknown[]>(xs: T) => xs; |
| 2 | const t = asTuple([1, "a", true]); |
Snippet 5 — json
Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.
| 1 | type 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.
| 1 | type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T]; |
| 2 | type 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.
| 1 | type RequireAtLeastOne<T> = { |
| 2 | [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>; |
| 3 | }[keyof T]; |
| 4 | type 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.
| 1 | type 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]; |
| 4 | type 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.
| 1 | type 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.
| 1 | type PublicOf<T> = { [K in keyof T]: T[K] }; |
| 2 | class Svc { #secret = 1; public open = true; } |
| 3 | type P = PublicOf<Svc>; |
note
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.