This TypeScript error occurs when you attempt to modify a property that has been marked as readonly. The readonly modifier prevents reassignment after initialization, enforcing immutability at the type level. Fixes include removing the readonly modifier if appropriate, creating new objects instead of mutating existing ones, or using utility types to work with mutable versions.
In TypeScript, the `readonly` keyword is a type modifier that marks properties as immutable after their initial assignment. This is a compile-time safety feature that helps prevent accidental mutations and enforces immutability patterns. When you declare a property as `readonly`, TypeScript enforces these rules: 1. **Initialization**: The property must be assigned a value at declaration or in the constructor (for classes) 2. **No reassignment**: Once initialized, the property cannot be assigned a new value 3. **Type-level enforcement**: This is purely a TypeScript compile-time check; JavaScript runtime allows the assignment The error "readonly property cannot be assigned to" (TS2540) occurs when you try to modify a readonly property after its initial assignment. This helps catch bugs where you might unintentionally change values that should remain constant throughout your program's execution.
First, locate the property causing the error. Look for the readonly keyword in type definitions:
// Interface with readonly properties
interface User {
readonly id: number; // This property is readonly
readonly name: string; // This property is readonly
email: string; // This property is mutable
}
const user: User = { id: 1, name: "John", email: "[email protected]" };
user.id = 2; // Error: readonly property cannot be assigned toIn classes:
class Config {
readonly apiUrl: string; // Readonly property
timeout: number; // Mutable property
constructor(url: string) {
this.apiUrl = url; // OK: assignment in constructor
this.timeout = 3000;
}
}
const config = new Config("https://api.example.com");
config.apiUrl = "https://new.com"; // Error: readonly property cannot be assigned toCheck the error message to identify the exact property name.
If you genuinely need to mutate the property and immutability isn't required, remove the readonly keyword:
// Before: readonly prevents mutation
interface User {
readonly id: number;
readonly name: string;
}
// After: properties are mutable
interface User {
id: number; // Now writable
name: string; // Now writable
}
const user: User = { id: 1, name: "John" };
user.id = 2; // Now allowed
user.name = "Jane"; // Now allowedConsiderations:
- Only remove readonly if the property truly needs to change
- Preserve readonly for properties that represent immutable identifiers (like IDs, keys, constants)
- Document why the property needs to be mutable
Instead of modifying readonly properties, create new objects with updated values using spread syntax:
interface User {
readonly id: number;
readonly name: string;
readonly email: string;
}
const user: User = { id: 1, name: "John", email: "[email protected]" };
// Don't mutate - create a new object
const updatedUser: User = {
...user,
email: "[email protected]", // New value for email
};
// Original user remains unchanged
console.log(user.email); // "[email protected]"
console.log(updatedUser.email); // "[email protected]"For nested objects:
interface Settings {
readonly theme: string;
readonly preferences: {
readonly notifications: boolean;
readonly language: string;
};
}
const settings: Settings = {
theme: "dark",
preferences: {
notifications: true,
language: "en"
}
};
// Create new object with updated nested property
const newSettings: Settings = {
...settings,
preferences: {
...settings.preferences,
language: "fr" // Update nested property
}
};This approach follows functional programming principles and avoids side effects.
Create a utility type that strips readonly modifiers when you need temporary mutability:
// Utility type to make all properties mutable
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
interface User {
readonly id: number;
readonly name: string;
}
// Create a mutable version for local manipulation
const user: Mutable<User> = { id: 1, name: "John" };
user.id = 2; // Allowed with Mutable type
user.name = "Jane"; // Allowed with Mutable type
// Convert back to readonly when done
const readonlyUser: User = user;For partial mutability:
// Make specific properties mutable
type MakeMutable<T, K extends keyof T> = Omit<T, K> & {
[P in K]: T[P];
};
interface User {
readonly id: number;
readonly name: string;
readonly email: string;
}
// Only make email mutable
type UserWithMutableEmail = MakeMutable<User, 'email'>;
const user: UserWithMutableEmail = { id: 1, name: "John", email: "[email protected]" };
user.email = "[email protected]"; // Allowed
// user.id = 2; // Still error: readonlyThis approach maintains type safety while allowing controlled mutations.
For readonly arrays, use non-mutating methods or create new arrays:
const readonlyArray: readonly string[] = ["a", "b", "c"];
// Error: push modifies the array
// readonlyArray.push("d");
// Solution 1: Create new array
const newArray = [...readonlyArray, "d"];
// Solution 2: Use non-mutating methods
const filtered = readonlyArray.filter(item => item !== "b");
// Solution 3: Convert to mutable temporarily
const mutableArray: string[] = [...readonlyArray];
mutableArray.push("d");For readonly tuples:
const readonlyTuple: readonly [number, string] = [1, "a"];
// Error: cannot modify tuple elements
// readonlyTuple[0] = 2;
// Create new tuple instead
const newTuple: readonly [number, string] = [2, readonlyTuple[1]];Remember: readonly string[] is different from ReadonlyArray<string> but both enforce immutability.
If you must bypass TypeScript's readonly check, use type assertions sparingly:
interface User {
readonly id: number;
readonly name: string;
}
const user: User = { id: 1, name: "John" };
// Type assertion to bypass readonly
(user as { id: number; name: string }).id = 2;
// More direct (but less safe)
(user as any).id = 3;
// Safer: assert to a mutable version of the same type
type MutableUser = { id: number; name: string };
(user as MutableUser).name = "Jane";Warnings:
- Type assertions bypass TypeScript's safety checks
- They don't change runtime behavior - JavaScript will still allow the mutation
- Use only when absolutely necessary and document why
- Consider if your design actually needs mutable properties instead
Better alternatives:
1. Refactor to use mutable types from the start
2. Use the Mutable utility type pattern
3. Create new objects instead of mutating
### Readonly at Compile Time vs Runtime
The readonly modifier is purely a TypeScript compile-time construct. JavaScript has no equivalent concept:
// TypeScript compile-time error
interface Point {
readonly x: number;
readonly y: number;
}
const point: Point = { x: 1, y: 2 };
point.x = 3; // TS2540: readonly property cannot be assigned to// Compiled JavaScript (runs without error)
const point = { x: 1, y: 2 };
point.x = 3; // Actually works!This means:
- Readonly provides development-time safety but not runtime protection
- For true runtime immutability, use Object.freeze()
- Type assertions can bypass readonly checks, so use them cautiously
### Readonly with 'as const' Assertions
The as const assertion makes all properties deeply readonly:
const config = {
api: {
url: "https://api.example.com",
timeout: 5000
},
features: ["auth", "upload"]
} as const;
// All properties become readonly
// config.api.url = "new-url"; // Error
// config.features.push("new"); // Error### Readonly in Class Constructors
In classes, readonly properties can only be assigned in the constructor:
class Database {
readonly connectionString: string;
readonly maxConnections: number;
constructor(conn: string, max: number = 10) {
// Only place where readonly properties can be assigned
this.connectionString = conn;
this.maxConnections = max;
}
updateConnection(newConn: string) {
// this.connectionString = newConn; // Error!
// Instead, return a new instance
return new Database(newConn, this.maxConnections);
}
}### Built-in Readonly Utilities
TypeScript provides several built-in utilities:
// Make all properties readonly
type ReadonlyUser = Readonly<{
id: number;
name: string;
}>;
// Readonly arrays (two equivalent syntaxes)
type ReadonlyStrings = readonly string[];
type AlsoReadonlyStrings = ReadonlyArray<string>;
// Readonly maps and sets
type ReadonlyMapType = ReadonlyMap<string, number>;
type ReadonlySetType = ReadonlySet<string>;### Performance Considerations
- Readonly has zero runtime cost - it's erased during compilation
- Creating new objects instead of mutating has memory overhead
- For performance-critical code, consider mutable designs with careful documentation
- Use readonly for public APIs to enforce contracts
- Use mutable types internally where performance matters
### Testing Readonly Code
When testing code with readonly properties:
1. Test that readonly properties cannot be reassigned (compile-time)
2. Test immutable operations create correct new objects
3. Use type assertions in tests to verify runtime behavior
4. Consider using Object.freeze() in tests to catch runtime mutations
// Test that demonstrates readonly behavior
interface TestData {
readonly value: number;
}
function processData(data: TestData): TestData {
// Should return new object, not mutate
return { value: data.value + 1 };
}
// Runtime check (optional)
const frozen = Object.freeze({ value: 1 });
const result = processData(frozen); // Should not throwFunction expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript