error TS2540: Cannot assign to 'id' because it is a read-only property. is reported when code writes to a property whose type says it cannot change. In JavaScript projects the read-only marking comes from three places: a @readonly tag on a class field, Object.freeze (whose result the checker types as Readonly), and Readonly<...> or readonly inside a JSDoc type.
For frozen objects the error matches what the running code does: in a module (always strict mode) assigning to a frozen property throws a TypeError. For @readonly fields it is a promise the checker keeps for you.
At a glance
| Code | TS2540 |
|---|---|
| Message | Cannot assign to '{0}' because it is a read-only property. |
| TypeScript's category | error |
| Listed under | Classes, JSX and unused code |
| Turned on by | No option: every checked file reports it |
| In .js files | Reported when the file is checked: checkJs in jsconfig.json, or // @ts-check at the top of the file |
The message is TypeScript's own text from its diagnostic messages file (Apache-2.0); {0} is filled in by tsc for each error.
What it looks like
export class Order {
/** @readonly */
id = crypto.randomUUID();
}
const order = new Order();
order.id = "custom-id";error TS2540: Cannot assign to 'id' because it is a read-only property.
const SHIPPING = Object.freeze({ standard: 4, express: 12 });
SHIPPING.express = 10;error TS2540: Cannot assign to 'express' because it is a read-only property.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} What causes it
- A
@readonlyfield is assigned outside the constructor. - A property of an
Object.freeze(...)result is assigned. - A value typed
Readonly<T>(or with areadonlyproperty) is modified.
How to fix it
Set it in the constructor, or make a changed copy
A @readonly field may be assigned in the class's constructor, so values that differ per instance go in as constructor arguments. For frozen data, build a new object with spread, { ...SHIPPING, express: 10 }, and leave the original alone.
export class Order {
/** @readonly */
id;
/** @param {string} [id] */
constructor(id = crypto.randomUUID()) {
this.id = id;
}
}
const order = new Order("custom-id");
console.log(order.id);
const SHIPPING = Object.freeze({ standard: 4, express: 12 });
export const holidayShipping = { ...SHIPPING, express: 10 };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Gotchas
- Read-only is shallow:
Object.freezeandReadonlyprotect the top-level properties, not the objects inside them. - Arrays have their own spelling,
readonly string[]orReadonlyArray<string>, which removespushand the other mutating methods; see read-only arrays.
Sources
- JSDoc Reference: @readonly · TypeScript handbook
- Object.freeze() · MDN Web Docs
- diagnosticMessages.json (TypeScript 5.9.3) · TypeScript on GitHub
Last reviewed 20 October 2026.