Menu

TypeScript readonly: Properties, Readonly<T> and Arrays

The readonly modifier and the Readonly<T> utility type stop code from reassigning properties. Learn readonly properties and class fields, Readonly<T>, readonly arrays (readonly T[] and ReadonlyArray), ReadonlyMap and ReadonlySet, why readonly is shallow and compile-time only, and how it compares with Object.freeze and as const.

This page includes runnable editors - edit, run, and see output instantly.

readonly marks a property that can be set once, when the object is created, and never reassigned. Readonly<T> applies it to every property of a type, and readonly T[] does the same for arrays:

The last line shows the most important fact about readonly: it is checked by the compiler, not enforced at runtime. The assignment was a compile error (suppressed here with @ts-expect-error), yet the emitted JavaScript still ran it. Without the suppression, the file would not compile, and that is where readonly does its job.

readonly Properties

Put readonly before a property name in an interface, a type literal or a class. The property can be initialized but not reassigned:

In a class, a readonly field can be assigned in its declaration or in the constructor, and nowhere else. The shortest form is a parameter property, constructor(readonly id: string) {}, which declares and assigns the field in one step. The classes page covers fields and constructors in general.

Readonly<T>: Every Property at Once

Readonly<T> is a utility type that marks every property of T as readonly. It is useful for values you pass around but should not change, such as application state:

The function signature tells the reader that addItem returns a new state instead of changing the old one, and the compiler holds the function to it. Readonly<T> is defined as the mapped type { readonly [P in keyof T]: T[P] }.

Readonly Arrays: readonly T[] and ReadonlyArray<T>

readonly number[] and ReadonlyArray<number> are the same type. They remove every mutating method (push, pop, shift, splice, sort, reverse, fill...) and forbid index assignment. The non-mutating methods remain and return ordinary arrays:

Taking readonly T[] as a parameter is a promise to callers that you will not modify their array. The other direction is where people get stuck: a readonly array cannot be passed to a function that takes a plain T[], because that function might mutate it.

index.ts(7,17): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.

The fix is to change sum to accept readonly number[], since it does not mutate anything. Functions that only read an array should always take the readonly type; then they accept both kinds. If you do not own the function, pass a copy: sum([...prices]).

ReadonlyMap and ReadonlySet

Maps and sets have readonly versions too. ReadonlyMap<K, V> has get, has, size, forEach and the iterators but no set, delete or clear; ReadonlySet<T> has no add, delete or clear:

A class often keeps a private mutable Map and exposes it through a getter typed ReadonlyMap, so outside code can read the data but not change it through that reference.

readonly Is Shallow

readonly and Readonly<T> protect only the property itself, not the object or array it points to:

DeepReadonly<T> applies itself to every nested object type, and since a mapped type over an array type produces a readonly array, members becomes readonly string[]. It is still a type-level promise, not runtime protection.

Compile-Time Only: Mutation Through Another Reference

A readonly type controls what one reference may do. Another reference to the same object, typed without readonly, can change it, and TypeScript even allows assigning a readonly type to a mutable one:

The assignment mutable = settings compiles because TypeScript does not factor in readonly properties when it checks whether two object types are compatible; the TypeScript handbook states this directly, and notes that readonly properties can therefore change through aliasing. Readonly arrays are different: the TS4104 error above is exactly that check. Object.freeze really prevents changes at runtime: the emitted code runs in strict mode, where writing to a frozen property throws a TypeError. Like readonly, Object.freeze is shallow.

readonly, const, as const and Object.freeze

as const on a literal makes every property readonly at every depth and keeps literal types, which is often the easiest way to get a deeply readonly value:

const theme = { mode: "dark", sizes: [12, 14] } as const;
// { readonly mode: "dark"; readonly sizes: readonly [12, 14] }
Applies toDeep?Runtime effectExample
consta variable bindingnothe variable cannot be reassignedconst user = {...}
readonlyone property or array typenononereadonly id: string
Readonly<T>every property of a typenononeReadonly<State>
as consta literal expressionyesnone{ ... } as const
Object.freezean object valuenowrites fail (throw in strict mode)Object.freeze(obj)

const and readonly answer different questions: const stops the name from pointing somewhere else, readonly stops a property from changing. A const object's properties can still be reassigned unless they are readonly.

Frequently Asked Questions

What does readonly do in TypeScript?

readonly marks a property that can be set when the object is created (or in a class constructor) but not reassigned afterwards. Assigning to it later is a compile error, TS2540. It is only a type check: the emitted JavaScript contains no protection.

What is the difference between readonly and const in TypeScript?

const is about a variable: the name cannot be pointed at a different value, but the object it holds can still be changed. readonly is about a property: that property cannot be reassigned. const user = { name: "Ada" } still allows user.name = "x"; a readonly name property does not.

How do I make an array readonly in TypeScript?

Annotate it as readonly T[] or ReadonlyArray<T> (the same type). Mutating methods such as push, pop, sort and splice disappear from the type, and index assignment is an error. Non-mutating methods like map, filter and slice still work and return ordinary arrays.

Is Readonly deep in TypeScript?

No. Readonly<T> and readonly only protect the top-level properties; nested objects and arrays inside can still be changed. Use as const on a literal, or write a recursive DeepReadonly<T> type, for deep protection at the type level.

Does readonly prevent changes at runtime?

No. Types are erased, so a readonly property is an ordinary property at runtime, and code with a mutable reference to the same object (or plain JavaScript) can still change it. Use Object.freeze when you need runtime protection; TypeScript types its result as Readonly<T>.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED