skip to content

In TypeScript, what is the difference between writing `as const` on an object literal and writing `satisfies SomeType` after it, and when would you use both together?

level: middleimportance: should knowfreq 47%

answer

  1. one pins, the other checks
  2. annotation validates then flattens
  3. orthogonal, so they compose
  4. satisfies is not a cast
  5. both erased at emit

basics

~20 s

They do different jobs. as const pins inference: literal types are kept and everything becomes readonly. satisfies checks the literal against a type without replacing the inferred type. Written together, as const satisfies T both pins the values and validates the shape.

solid answer

~40 s

`as const` is about **inference**: it stops widening, marks properties `readonly`, and turns array literals into readonly tuples. It checks the value against nothing. `satisfies T`, available since TypeScript 4.9, is about **validation**: the compiler verifies the expression is assignable to `T` — including excess-property checking — but the variable keeps its own narrower inferred type instead of being widened to `T`. That is the key contrast with a plain annotation `const x: T = {...}`, which validates but also throws away the specific keys and literal values. So use `as const` when the exact values matter downstream, `satisfies` when you want a contract enforced without losing precision, and `as const satisfies T` when you want both: pinned literals plus a compile-time guarantee that every entry has the required shape.

code

typescript · 16 lines
typescript
type Route = { path: string; auth: boolean };

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

// checked against Route, and still precise:
type RouteName = keyof typeof routes; // "home" | "admin"
const adminPath: '/admin' = routes.admin.path;

// the annotation form loses that precision:
const flattened: Record<string, Route> = {
  home: { path: '/', auth: false },
};
const anyPath: string = flattened.home.path;

go deeper

for a junior

Know that both are written next to a literal but do opposite things: one keeps the exact values, the other checks the value against a type you name.

for a middle

Be ready to compare all three forms — annotation, as const, satisfies — on the same literal and say what each does to the resulting type, including that an annotation discards the specific keys and values.

for a senior

Show that you reach for the combined form on configuration tables and can explain the assignability subtlety that lets a readonly literal satisfy a mutable-shaped target.

for a principal

Set the house style: where contracts are enforced with satisfies versus annotated boundaries, and how much inferred precision you are willing to expose from a shared package, since precise inferred types become part of what consumers depend on.

## Three ways to attach a type to a literal Take a routes table and compare what each form gives you. Assume TypeScript 4.9 or later, which is when the `satisfies` operator became available. ```ts type Route = { path: string; auth: boolean }; // A. annotation const a: Record<string, Route> = { home: { path: '/', auth: false } }; // B. const assertion const b = { home: { path: '/', auth: false } } as const; // C. satisfies const c = { home: { path: '/', auth: false } } satisfies Record<string, Route>; ``` **A — the annotation validates and then flattens.** The compiler checks the literal against `Record<string, Route>`, and from then on `a` *is* that type. `a.home` is `Route`, `a.home.path` is `string`, and `keyof typeof a` is just `string` — you have lost the fact that the only key is `home`. Any typo in a key name is also accepted, because the index signature admits every string. **B — the assertion pins and validates nothing.** `b.home.path` is the literal `"/"`, `keyof typeof b` is `"home"`, and everything is `readonly`. But nothing checked that the entries look like a `Route`; misspell `auth` as `athu` and it compiles happily. **C — `satisfies` validates without flattening.** The expression is checked for assignability to `Record<string, Route>` — a missing or misspelled property is an error, and excess-property checking applies to the fresh object literal — yet `c` keeps its inferred type. `keyof typeof c` is still `"home"`. The values are not `readonly` and are still widened (`c.home.path` is `string`), because `satisfies` does not touch widening. ## Combining them The two are orthogonal, which is why they compose: ```ts const routes = { home: { path: '/', auth: false }, admin: { path: '/admin', auth: true }, } as const satisfies Record<string, Route>; type RouteName = keyof typeof routes; // "home" | "admin" const p: '/admin' = routes.admin.path; // literal preserved ``` Now a missing `auth` is a compile error *and* the exact key names and path strings survive for downstream derivation. This is the workhorse form for configuration tables. One subtlety worth understanding: assigning a value whose properties are `readonly` to a target type whose properties are mutable is allowed — TypeScript deliberately ignores `readonly` modifiers when checking property assignability. That is why `as const satisfies Record<string, Route>` passes even though `Route`'s members are not declared `readonly`. ## Choosing between them - **Only `as const`** — a fixed list or tuple whose shape is self-evident and whose exact values you will derive from. A list of allowed option strings, a coordinate pair, an action-type table. - **Only `satisfies`** — you want a contract enforced but the value is genuinely mutable, or the extra precision buys you nothing. Also useful to catch typos in keys of a table that is otherwise open. - **Both** — a constants table that must obey a shape *and* whose keys/values feed derived types. - **Plain annotation** — the flattened type is what you actually want to expose, for instance a public value whose precise contents should not leak into consumers' types. ## Common misconceptions `satisfies` is not a cast. `as T` asserts, suppressing checks and shifting the type by force; `satisfies T` only checks and never changes the type. If the expression is not assignable to `T`, `satisfies` errors where `as` would have silently accepted a wrong assertion. `satisfies` also does not make anything `readonly`, and `as const` does not make anything type-checked. Candidates who describe them as "two ways to type a constant" have missed that each covers exactly the gap the other leaves. ## What is emitted Neither survives compilation. Both are type-layer constructs and are erased, so the runtime value is the plain object literal in all three forms above. The choice affects what the checker knows and what your editor shows — nothing else.

  • Why not just annotate the constant with the type instead of using `satisfies`?
    An annotation replaces the inferred type with the annotated one. You get validation, but the variable is now the general type: exact keys, literal values and tuple lengths are gone, so nothing precise can be derived from it. `satisfies` gives the same validation while leaving the narrow inferred type in place.
  • Does `satisfies` catch a misspelled property in an object literal?
    Yes. The expression must be assignable to the target type, and a fresh object literal is also subject to excess-property checking, so an unexpected key is flagged. A missing required property fails assignability outright. That is precisely the safety a bare `as const` does not give you.
  • If `as const` makes properties readonly, how can the value still satisfy a type whose properties are mutable?
    TypeScript ignores `readonly` property modifiers when checking assignability between object types — a known and deliberate hole in the model. So a fully readonly literal is assignable to a mutable-shaped target, which is what makes `as const satisfies T` usable in practice.

saying these in an interview costs you the question

  • Calls satisfies a safer form of the as cast
  • Thinks satisfies makes the value readonly
  • Thinks as const type-checks the literal against something
  • Says an annotation and satisfies are interchangeable
  • Believes one of them emits a runtime check

context