Readonly and const Assertions
Freeze intent with readonly, Readonly<T>, and as const — keep literal types, prevent accidental mutation, and type config objects correctly.
- 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
- Thinking
readonlyis runtime —Object.freezeis separate. - Spreading readonly arrays into mutable — type may widen.
- Over-freezing mutable draft state — hard to update; use for published configs.
as conston 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.”
Related on this site
- Literal types
- satisfies operator
- Enums vs union types
- Utility types Partial Pick Omit
- keyof typeof and indexed access
Further reading
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.