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 constwhen 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.
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