Stockio

TypeScript satisfies vs as const: Keep Type Inference

Use satisfies, as const, and explicit annotations deliberately to check configuration objects while preserving useful literal values and catching misspelled properties.

By Sofia Marchetti

JavaScript Engineer ·

On this page

A configuration object often serves two purposes: it must satisfy a shared contract, and its particular values should remain available as precise types. A broad annotation can discard useful details, while an assertion can hide a mistake. TypeScript's satisfies operator provides another option, but it solves a different problem from as const.

Start with a configuration contract

Consider a set of named export presets. Each preset has a format and a quality value, while the application needs to derive the available preset names. This is configuration typing: check the expected structure without unnecessarily replacing the expression's inferred type.

Open the JSON formatter with a representative configuration snapshot to review spelling, nesting, and repeated keys before converting the example into TypeScript. The formatter checks JSON syntax, not TypeScript operators, and JSON cannot contain comments or const assertions.

Decide whether the list of preset names is closed. If every application must define exactly preview and archive, use an explicit union of those names. If each application can define its own names, use a string-keyed record and derive the names from the resulting object. Those are different contracts, not interchangeable stylistic preferences.

Compare annotations, assertions, and satisfies

An annotation declares the variable's type. An assertion asks the compiler to treat an expression as a chosen type, within its compatibility rules. The satisfies operator checks assignability while retaining the expression's resulting type rather than substituting the target annotation.

type Preset = {
format: "jpeg" | "webp";
quality: number;
};

const presets = {
preview: { format: "webp", quality: 75 },
archive: { format: "jpeg", quality: 95 },
} satisfies Record<string, Preset>;

type PresetName = keyof typeof presets;
const format = presets.preview.format;

PresetName is derived from the actual keys. A misspelled required field inside a preset produces a diagnostic. The TypeScript 4.9 release notes introduce satisfies with examples of this balance between checking and inference.

Do not summarize the operator as never changing inference. Its target type can supply contextual typing, which influences the expression. The practical promise is that it does not simply replace the expression's type with the target type as an annotation would.

Use as const for literal preservation

A const variable binding prevents reassignment of the variable, not mutation of the object it references. A const assertion narrows literal values and gives object properties readonly types. For array literals, it also produces readonly tuples rather than ordinary mutable arrays.

const formats = ["jpeg", "webp"] as const;
type Format = typeof formats[number];

const fixedPresets = {
preview: { format: "webp", quality: 75 },
} as const satisfies Record<string, Preset>;

Now quality retains the literal type 75, which may be useful when deriving metadata but inconvenient if code needs to assign a different number later. Choose literal preservation only where those exact values are meaningful to the design.

The readonly effect is a type-system rule, not a runtime freeze. Another reference or untyped JavaScript can still mutate the underlying object. If runtime immutability is required, consider freezing appropriate objects and understand that a shallow freeze does not recursively freeze nested structures.

See the const assertion release notes for the distinction between literal widening and readonly containers. Do not substitute const assertions for a deliberate ownership policy.

Choose closed keys when omissions are bugs

A record with arbitrary string keys checks each provided value but does not require particular names. If both preview and archive are mandatory, define a union and check against Record<RequiredName, Preset> instead.

This catches missing required entries and, for a fresh object literal, unexpected keys. That extra-property checking is valuable, but it does not make TypeScript an exact-object system. Values stored in other variables can carry additional properties and still satisfy structurally compatible types.

Be explicit about open versus closed keys in the public contract. A plugin registry often needs open keys. A translation file for a fixed set of labels often needs closed keys so missing translations fail during development. The same Record utility supports both, depending on its key parameter.

Avoid immediately widening your carefully checked object by assigning it to a broad variable elsewhere. Derive the public name union before passing data into APIs that intentionally erase details. Keep the precise source definition and the broad consumer-facing contract separate when that improves readability.

Verify mutation and external boundaries

Write examples that exercise both correct use and likely mistakes. Try an unsupported format, a missing quality value, a misspelled preset name, and an attempted mutation. The expected diagnostics reveal whether you need satisfies alone, as const alone, or their combination.

For runtime input, neither operator performs validation. An imported user file, localStorage value, or fetched response needs runtime checks. TypeScript removes these operators from emitted JavaScript; they cannot reject a malformed object after deployment.

The TypeScript everyday types guide explains why assertions do not add runtime checking. Keep that distinction visible in code review, especially when a configuration source changes from a checked source file to remotely editable data.

Document the pattern with related TypeScript examples and your team's development references. Include a short explanation of why literal precision matters in this particular registry. Without that explanation, future maintainers may replace a helpful operator combination with a broad assertion simply because both versions appear to compile.

Separate registry definitions from editable values

Suppose a settings screen lets users adjust export quality. The registry defines preview quality as the literal 75, but the editable form must accept 60, 80, or another supported number. Do not mutate the const-asserted registry or cast away readonly modifiers. Instead, create a separate working value explicitly typed as Preset and initialize it from the selected entry. The registry retains precise metadata while the form has the broader type its behavior requires.

This distinction also matters for arrays. A const-asserted list of allowed formats becomes readonly, so a contract demanding a mutable array may reject it. If consumers only iterate, make the contract accept a readonly array. If they intentionally sort or append, provide a mutable copy. Changing the contract should reflect ownership, not merely make an inconvenient diagnostic disappear.

Review the definition with three concrete questions: should preset names be derived or prescribed, should values remain exact literals or be editable, and should consumers be allowed to mutate shared data? These ownership decisions determine the operator combination. They also explain why two configuration objects in the same codebase may correctly use different patterns.

For numeric constraints such as quality between zero and one hundred, a number property alone is insufficient. Add runtime checks wherever users or remote systems can supply values. A checked source definition and a validated editable form complement each other; neither replaces the other's responsibility.

Key takeaways

  • Use satisfies to check a contract without broad annotation replacement.
  • Use as const when exact literals and readonly types are useful.
  • Choose closed key unions when missing entries are bugs.
  • Remember that readonly typing does not freeze runtime objects.
  • Validate externally loaded configuration at runtime.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in JavaScript & Frameworks
JavaScript & Frameworks

10 Best JavaScript Charting Libraries

Compare ten JavaScript chart libraries for dashboards, reports, and interactive data stories. See where each excels, what it costs, and how to choose based on your app and team.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

8 Best React State Management Libraries

Find a React state approach that fits your app, from built-in hooks to dedicated stores, with clear trade-offs around data flow, debugging, async work, and learning effort.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

12 Best and Most Popular JavaScript Libraries

Compare twelve widely used JavaScript libraries for interfaces, data, testing, and everyday development, with clear strengths, trade-offs, and advice for choosing only what your project needs.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

localStorage vs sessionStorage vs IndexedDB Compared

Choose browser storage by data lifetime, size and access pattern. Compare synchronous Web Storage with IndexedDB transactions, migrations, quotas and failure handling.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

React Server Components Explained for Developers

Learn what React Server Components execute, what crosses the client boundary, and how to structure data access, interactive widgets, and mutations without leaking private data.

Includes: JSON formatterDaniel KimDaniel Kim
JavaScript & Frameworks

useMemo vs useCallback vs React.memo: Fewer Rerenders

Learn when React memoization helps, how prop identity affects rerenders, and why profiling and state placement should come before useMemo and useCallback.

Includes: JSON formatterDaniel KimDaniel Kim