Lessons
TypeScript Tutorial
TypeScript Control Structures
TypeScript Function Types
readonly in TypeScript
What is TypeScript readonly
The readonly
modifier in TypeScript is used to mark properties as immutable after their initial assignment. This immutability applies to properties within classes, interfaces, and types. By using readonly
, developers can prevent accidental modifications to these properties, leading to more maintainable and error-resistant code.
TypeScript readonly in Classes
In classes, the readonly
modifier ensures that a property can be assigned a value only once, either during its declaration or within the constructor. Attempting to modify a readonly
property outside these contexts results in a compile-time error.
Example:
tsx
1 2 3 4 5 6 7 8 9 10 11 12 13
class Employee { readonly empCode: number; empName: string; constructor(code: number, name: string) { this.empCode = code; this.empName = name; } } let emp = new Employee(10, "John"); emp.empCode = 20; // Compiler Error: Cannot assign to 'empCode' because it is a read-only property. emp.empName = 'Bill'; // This is allowed since empName is not readonly.
In this example, empCode
is a readonly
property. It can be initialized either at its declaration or within the constructor. Any attempt to modify empCode
after the object has been instantiated will result in a compile-time error.
Implement readonly Interfaces
Interfaces can also define readonly
properties, ensuring that implementing objects cannot modify these properties once they are set.
Example:
tsx
1 2 3 4 5 6 7 8 9 10 11
interface IEmployee { readonly empCode: number; empName: string; } let empObj: IEmployee = { empCode: 1, empName: "Steve" }; empObj.empCode = 100; // Compiler Error: Cannot assign to 'empCode' because it is a read-only property.
Here, empCode
is marked as readonly
within the IEmployee
interface. While it can be assigned a value when creating the object empObj
, any subsequent attempts to modify empCode
will trigger a compile-time error.
The Readonly<T> Utility Type
TypeScript provides a utility type Readonly<T>
that transforms all properties of a type T
into readonly
properties. This is particularly useful when you want to enforce immutability across an entire object type without individually marking each property as readonly
.
Example:
tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
interface IEmployee { empCode: number; empName: string; } let emp1: Readonly<IEmployee> = { empCode: 1, empName: "Steve" }; emp1.empCode = 100; // Compiler Error: Cannot assign to 'empCode' because it is a read-only property. emp1.empName = 'Bill'; // Compiler Error: Cannot assign to 'empName' because it is a read-only property. let emp2: IEmployee = { empCode: 1, empName: "Steve" }; emp2.empCode = 100; // This is allowed since emp2 is not Readonly<IEmployee>. emp2.empName = 'Bill'; // This is allowed since emp2 is not Readonly<IEmployee>.
In this example, emp1
is of type Readonly<IEmployee>
, making all its properties immutable. Any attempt to modify empCode
or empName
results in a compile-time error. Conversely, emp2
is a regular IEmployee
object, allowing property modifications.
Readonly Arrays and Tuples
TypeScript extends the concept of immutability to arrays and tuples using the readonly
modifier. A ReadonlyArray<T>
is an array that cannot be modified after creation, preventing operations like push, pop, or direct element assignment.
Example:
tsx
1 2 3
let arr: ReadonlyArray<number> = [1, 2, 3]; arr.push(4); // Compiler Error: Property 'push' does not exist on type 'readonly number[]'. arr[0] = 10; // Compiler Error: Index signature in type 'readonly number[]' only permits reading.
Similarly, TypeScript 3.4 introduced the ability to declare readonly
tuples, ensuring that the contents of the tuple remain unchanged.
Example:
tsx
1 2
let point: readonly [number, number] = [0, 0]; point[0] = 1; // Compiler Error: Index signature in type 'readonly [number, number]' only permits reading.
By using ReadonlyArray
and readonly
tuples, developers can enforce immutability on array and tuple data structures, preventing unintended modifications.
Limitations and Considerations
While the readonly
modifier prevents reassignment of properties, it does not make the value itself immutable, especially if the value is an object. This means that while you cannot reassign the property to a new object, you can still modify the internal state of the object.
Example:
tsx
1 2 3 4 5 6 7 8 9 10
interface Home { readonly resident: { name: string; age: number }; } const home: Home = { resident: { name: "John", age: 30 } }; home.resident.age = 31; // This is allowed. home.resident = { name: "Doe", age: 40 }; // Compiler Error: Cannot assign to 'resident' because it is a read-only property.
In this case, while the resident
property is readonly
and cannot be reassigned, the properties of the resident
object itself can still be modified. To achieve true immutability, you would need to use additional tools or techniques such as freezing the object using Object.freeze()
.
Example with Object.freeze()
:
tsx
1 2
const frozenResident = Object.freeze({ name: "John", age: 30 }); frozenResident.age = 31; // This results in a runtime error in strict mode.
This ensures that both the object reference and its internal state cannot be modified.
Practical Examples
Example 1: Combine readonly with Constructor Parameters
Using readonly
directly on constructor parameters reduces boilerplate code by automatically creating and initializing readonly
properties.
Code Example:
tsx
1 2 3 4 5 6 7 8
class Student { constructor(public readonly id: number, public name: string) {} } const student = new Student(1, "Alice"); console.log(student.id); // Output: 1 student.name = "Bob"; // Allowed student.id = 2; // Compiler Error: Cannot assign to 'id' because it is a read-only property.
Here, id
is automatically a readonly
property without explicitly declaring it.
Example 2: Readonly with Nested Objects
The Readonly<T>
utility type applies only to the top level of the object. If you need deep immutability, you would need a recursive solution or libraries like Immutable.js
.
Code Example:
tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
interface Person { readonly name: string; readonly address: { street: string; city: string; }; } const person: Readonly<Person> = { name: "John", address: { street: "123 Elm St", city: "Springfield" }, }; person.address.street = "456 Oak St"; // This is allowed because `Readonly` is shallow. person.address = { street: "789 Pine St", city: "Rivertown" }; // Compiler Error
Example 3: ReadonlyArray Usage in Functions
Functions that should not modify an array passed to them can use ReadonlyArray
to enforce immutability.
Code Example:
tsx
1 2 3 4 5 6 7
function displayNames(names: ReadonlyArray<string>): void { names.push("New Name"); // Compiler Error console.log(names.join(", ")); } const names: ReadonlyArray<string> = ["Alice", "Bob", "Charlie"]; displayNames(names);
This approach ensures that the original array remains unchanged.
Conclusion
The readonly
modifier in TypeScript is a versatile and essential tool for enforcing immutability in your code. By marking properties, arrays, and types as readonly
, you can prevent accidental modifications, enhance code safety, and create more predictable software.
Key Takeaways
- Use
readonly
in classes, interfaces, and arrays to enforce immutability. - Leverage the
Readonly<T>
utility type for transforming all properties of an object type intoreadonly
. - Remember that
readonly
is shallow; additional measures are required for deep immutability. - Use
ReadonlyArray
andreadonly
tuples to handle immutable lists and fixed-size collections.