TypeScript6 min read

Const Assertions Preserve Literals, Not Runtime Immutability

How as const, readonly tuples, spreads, and const type parameters influence inference.

  • const assertions
  • readonly
  • type inference

JavaScript’s const prevents rebinding a variable; it does not freeze the referenced object. TypeScript adds several similarly named features—readonly, as const, and const type parameters—that influence static inference rather than runtime mutation.

const request = {
  method: 'GET',
  retry: false,
} as const;

Without the assertion, object properties are inferred broadly enough to support later assignment: method becomes string. With as const, literal expressions retain literal types and object properties become readonly.

Const assertions are shallow at runtime

as const emits no Object.freeze call. JavaScript code, an any escape, or mutation through another alias can still change the value.

const mutable = { count: 0 };
const snapshot = { nested: mutable } as const;

mutable.count += 1; // allowed
console.log(snapshot.nested.count); // 1

The assertion makes the nested property readonly, so it cannot be pointed at another object through snapshot. It does not retroactively make the separately declared mutable object deeply readonly.

When every nested object and array is written directly inside one const-asserted literal, TypeScript recursively preserves those literal structures. That still describes types only; use runtime freezing or immutable data practices when hostile mutation must be prevented.

Arrays become readonly tuples

const route = ['users', 42] as const;
// readonly ['users', 42]

The tuple records length, position, and literal types. A function that only reads values should accept a readonly tuple or readonly T[], allowing both mutable and readonly callers.

A readonly tuple is assignable to a mutable tuple with the same element types because assignment copies only the reference, not the readonly qualifier. The receiving function may mutate it, and TypeScript trusts the caller not to observe that mutation.

Spreading changes the inferred container

Spreading a readonly tuple into a new array produces a fresh mutable value:

const pair = ['left', 'right'] as const;
const copy = [...pair];
// ('left' | 'right')[]

The element literals remain useful, but tuple length and positions are not preserved by default. Add another const assertion when the new expression should remain a readonly tuple: const copy = [...pair] as const.

Const type parameters preserve caller detail

TypeScript can request const-like inference in a generic API:

function defineRoutes<const T extends string[]>(routes: T): T {
  return routes;
}

const routes = defineRoutes(['/', '/account']);
// readonly ['/', '/account']

The caller does not need as const at the call site. The modifier changes inference for expressions written there; it does not force a mutable variable passed later to become readonly, and it does not freeze the returned array.

These tools preserve information that ordinary widening would discard. Use them when literal values and tuple positions form part of an API contract, while keeping runtime immutability as a separate engineering decision.