TypeScript: Readonly and Utility Modifiers
One of the common frontend interview questions that focuses on code styling and clean architecture is:
How do you declare immutable properties in TypeScript? What is the difference between
readonly, theReadonly<T>utility type, andas constassertions?
TypeScript provides multiple mechanisms to enforce compile-time immutability, allowing you to catch unintended side effects (like mutating state objects directly instead of using setState/reducers).
1. The readonly Property Modifier
The readonly keyword is applied to individual properties in interfaces, type aliases, or classes:
interface User {
readonly id: string;
name: string;
}
const user: User = { id: "123", name: "Alice" };
user.name = "Bob"; // ✅ Allowed
user.id = "456"; // ❌ Compile Error: Cannot assign to 'id' because it is a read-only property.Class usage:
class Config {
readonly apiUrl: string;
constructor(url: string) {
this.apiUrl = url; // ✅ Allowed only in constructors
}
}2. The Readonly<T> Utility Type
Instead of manually adding readonly to every key in a large object shape, you can wrap the entire type in the Readonly<T> utility type, which maps all properties to read-only recursively.
interface State {
theme: string;
isLoading: boolean;
}
const state: Readonly<State> = {
theme: "dark",
isLoading: false
};
state.theme = "light"; // ❌ Compile Error: Cannot assign to 'theme' because it is a read-only property.3. Readonly Arrays (ReadonlyArray<T> / readonly Type[])
Standard arrays in JavaScript are mutable (methods like push(), splice(), and sort() modify the original array in place). TypeScript provides a specific immutable array type:
const numbers: readonly number[] = [1, 2, 3];
// Alternative: const numbers: ReadonlyArray<number> = [1, 2, 3];
numbers.push(4); // ❌ Compile Error: Property 'push' does not exist on type 'readonly number[]'.
numbers[0] = 10; // ❌ Compile Error: Index signature in type 'readonly number[]' only permits reading.4. readonly vs. const vs. as const
It is common to confuse these constructs. Here is how they stack up:
const: A standard JavaScript runtime variable declaration. It prevents variable re-assignment, but does not prevent mutating the object's properties.readonly: A TypeScript compiler flag. It prevents property reassignment on an object at compile-time.as const: A const assertion. It locks down both the array/object references, keeps literal types from widening, and recursively sets all nested properties toreadonly.
const config = {
port: 8080
} as const;
config.port = 9000; // ❌ Compile Error: Cannot assign because it is read-onlyKey Contrast Summary
| Feature | const variable | readonly property | as const assertion |
|---|---|---|---|
| System Level | JavaScript Runtime | TypeScript Compiler | TypeScript Compiler |
| Scope | Variable Reference | Object Property | Entire Literal Expression |
| Deep Immutability | ❌ No | ❌ No (requires nesting) | ✅ Yes (recursively sets all to read-only) |
| Type Widening | ✅ Widen strings to string | ✅ Widened | ❌ Kept as strict literal types |
Key Takeaways
- readonly Modifier: Prevents properties on an interface or class from being reassigned after initialization.
- Readonly Utility: Wraps an entire object type to make all its properties read-only without manual keys.
- as const Assertions: Locks down literal values recursively to read-only, preventing type widening to primitive types.
- ReadonlyArray: Prevents mutating array methods like
pushandpopfrom running on an array at compile-time.