ESC

Type to search the knowledge base.

Readonly and const Assertions

Freeze intent with readonly, Readonly<T>, and as const — keep literal types, prevent accidental mutation, and type config objects correctly.

intermediate3 min read
  • typescript
  • readonly-and

Mutation bugs in shared state are common. TypeScript’s readonly and as const don’t make objects immutable at runtime, but they stop your code from assigning to properties the type system considers fixed — and as const preserves literal types that would otherwise widen.

Docs: const assertions, Readonly utility.

readonly properties

type User = {
  readonly id: string;
  name: string;
};

const u: User = { id: '1', name: 'Ada' };
u.name = 'Grace'; // ok
// u.id = '2'; // error

IDs, route params, and design tokens are good readonly candidates.

Readonly<T> and ReadonlyArray<T>

type ReadonlyUser = Readonly<User>;
// all props readonly

function sum(nums: readonly number[]): number {
  // nums.push(1); // error
  return nums.reduce((a, b) => a + b, 0);
}

readonly T[] / ReadonlyArray<T> prevent push/pop through the type. Runtime arrays remain mutable if cast away.

as const

const routes = {
  home: '/',
  docs: '/docs',
} as const;

// typeof routes:
// { readonly home: '/'; readonly docs: '/docs' }

Without as const, home is string. With it, you get literal unions for keys and values — ideal for routers and status maps.

const STATUSES = ['idle', 'loading', 'done'] as const;
type Status = (typeof STATUSES)[number]; // 'idle' | 'loading' | 'done'

as const on tuples

const pair = [1, 'a'] as const; // readonly [1, 'a']

Useful for useState patterns and variadic helpers.

readonly vs const variable

const point = { x: 1, y: 2 };
point.x = 3; // ok — const blocks rebinding, not mutation

const pointRO: Readonly<{ x: number; y: number }> = { x: 1, y: 2 };
// pointRO.x = 3; // error

Explain this in interviews — people confuse binding const with deep immutability.

Deep readonly (manual / lib)

Readonly<T> is shallow:

type Nested = Readonly<{ child: { n: number } }>;
// nested.child = … // error
// nested.child.n = 2; // still ok

Use recursive helpers or libraries (type-fest ReadonlyDeep) when needed — or prefer immutable update patterns in app state (Redux/Immer discipline).

Props that shouldn’t be mutated

type Props = {
  readonly items: readonly Item[];
  onSelect: (id: string) => void;
};

Signals intent: parent owns the array. Combined with lint rules against param reassignment, this reduces accidental child mutations.

satisfies + as const

type Config = { url: string; retries: number };

const config = {
  url: 'https://api.example.com',
  retries: 3,
} as const satisfies Config;

Keeps literals while verifying shape — see satisfies operator.

Footguns

  1. Thinking readonly is runtime — Object.freeze is separate.
  2. Spreading readonly arrays into mutable — type may widen.
  3. Over-freezing mutable draft state — hard to update; use for published configs.
  4. as const on dynamic data — only for compile-time known values.
// still mutable at runtime
const frozenType = { a: 1 } as const;
(frozenType as { a: number }).a = 2; // compiles with assertion — runtime mutates

Interview out-loud answer

“readonly and Readonly<T> prevent typed mutation; as const preserves literal types and makes props readonly. const only stops rebinding. I use as const for config maps and status lists, and I don’t pretend it’s deep immutability without freeze or immutable patterns.”

Further reading

Related guides