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 to | Deep? | Runtime effect | Example | |
|---|---|---|---|---|
const | a variable binding | no | the variable cannot be reassigned | const user = {...} |
readonly | one property or array type | no | none | readonly id: string |
Readonly<T> | every property of a type | no | none | Readonly<State> |
as const | a literal expression | yes | none | { ... } as const |
Object.freeze | an object value | no | writes 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>.