TypeScript provides tools for enforcing immutability at the type level. Here's how to use them effectively.
Readonly Properties#
1// Readonly property modifier
2interface User {
3 readonly id: number;
4 name: string;
5 email: string;
6}
7
8const user: User = {
9 id: 1,
10 name: 'John',
11 email: 'john@example.com'
12};
13
14user.name = 'Jane'; // OK
15user.id = 2; // Error: Cannot assign to 'id'
16
17// Class readonly properties
18class Config {
19 readonly apiUrl: string;
20 readonly maxRetries: number;
21
22 constructor(url: string, retries: number) {
23 this.apiUrl = url;
24 this.maxRetries = retries;
25 }
26}
27
28const config = new Config('https://api.example.com', 3);
29config.apiUrl = 'new-url'; // Error: Cannot assign to 'apiUrl'Readonly<T> Utility Type#
1// Make all properties readonly
2interface MutableUser {
3 id: number;
4 name: string;
5 settings: {
6 theme: string;
7 language: string;
8 };
9}
10
11type ReadonlyUser = Readonly<MutableUser>;
12
13const user: ReadonlyUser = {
14 id: 1,
15 name: 'John',
16 settings: { theme: 'dark', language: 'en' }
17};
18
19user.name = 'Jane'; // Error
20user.settings = {}; // Error
21user.settings.theme = 'light'; // OK! Shallow readonly
22
23// Deep readonly
24type DeepReadonly<T> = {
25 readonly [P in keyof T]: T[P] extends object
26 ? DeepReadonly<T[P]>
27 : T[P];
28};
29
30type FullyReadonlyUser = DeepReadonly<MutableUser>;ReadonlyArray<T>#
1// Readonly array
2const numbers: ReadonlyArray<number> = [1, 2, 3];
3// or: readonly number[]
4
5numbers.push(4); // Error: Property 'push' does not exist
6numbers[0] = 10; // Error: Index signature only permits reading
7numbers.length = 0; // Error
8
9// Allowed operations
10const first = numbers[0]; // OK
11const doubled = numbers.map(n => n * 2); // OK (returns new array)
12const filtered = numbers.filter(n => n > 1); // OK
13
14// Function parameter
15function processItems(items: readonly string[]) {
16 // Can't modify items
17 items.push('new'); // Error
18 return items.join(', '); // OK
19}Const Assertions#
1// as const makes literal types
2const point = { x: 10, y: 20 } as const;
3// Type: { readonly x: 10; readonly y: 20; }
4
5point.x = 15; // Error: Cannot assign to 'x'
6
7// Array becomes readonly tuple
8const colors = ['red', 'green', 'blue'] as const;
9// Type: readonly ['red', 'green', 'blue']
10
11colors.push('yellow'); // Error
12colors[0] = 'orange'; // Error
13
14// Literal types preserved
15type Colors = typeof colors[number];
16// 'red' | 'green' | 'blue'
17
18// Without as const
19const colorsNormal = ['red', 'green', 'blue'];
20// Type: string[]Object Literals with as const#
1// Config object
2const config = {
3 api: {
4 baseUrl: 'https://api.example.com',
5 timeout: 5000,
6 },
7 features: {
8 darkMode: true,
9 notifications: false,
10 },
11} as const;
12
13// All properties are readonly and literal types
14type Config = typeof config;
15// {
16// readonly api: {
17// readonly baseUrl: "https://api.example.com";
18// readonly timeout: 5000;
19// };
20// readonly features: {
21// readonly darkMode: true;
22// readonly notifications: false;
23// };
24// }
25
26// Extract literal values
27type BaseUrl = typeof config.api.baseUrl;
28// "https://api.example.com"Enum-like Constants#
1// Instead of enum, use as const
2const Status = {
3 Pending: 'pending',
4 Active: 'active',
5 Completed: 'completed',
6} as const;
7
8type Status = typeof Status[keyof typeof Status];
9// 'pending' | 'active' | 'completed'
10
11function updateStatus(status: Status) {
12 console.log(status);
13}
14
15updateStatus(Status.Active); // OK
16updateStatus('pending'); // OK
17updateStatus('invalid'); // Error
18
19// With numeric values
20const HttpStatus = {
21 Ok: 200,
22 NotFound: 404,
23 ServerError: 500,
24} as const;
25
26type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus];
27// 200 | 404 | 500Tuple Types#
1// Regular array
2const arr = [1, 'hello'];
3// Type: (string | number)[]
4
5// Const assertion creates tuple
6const tuple = [1, 'hello'] as const;
7// Type: readonly [1, 'hello']
8
9// Explicit tuple type
10const point: [number, number] = [10, 20];
11point[0] = 15; // OK, not readonly
12
13// Readonly tuple
14const readonlyPoint: readonly [number, number] = [10, 20];
15readonlyPoint[0] = 15; // Error
16
17// Function returning tuple
18function getCoordinates(): readonly [number, number] {
19 return [10, 20] as const;
20}Readonly in Functions#
1// Readonly parameter
2function processUsers(users: readonly User[]) {
3 // Can't modify the array
4 users.push(newUser); // Error
5 users.sort(); // Error
6
7 // Can read and create new arrays
8 return users.filter(u => u.active);
9}
10
11// Return readonly
12function getConfig(): Readonly<Config> {
13 return {
14 apiKey: 'secret',
15 timeout: 5000,
16 };
17}
18
19const config = getConfig();
20config.apiKey = 'new'; // Error
21
22// Generic readonly function
23function freeze<T>(obj: T): Readonly<T> {
24 return Object.freeze(obj);
25}Satisfies with as const#
1// satisfies + as const for type checking with literals
2const routes = {
3 home: '/',
4 about: '/about',
5 users: '/users',
6} as const satisfies Record<string, string>;
7
8// Type preserves literal strings
9type Route = typeof routes[keyof typeof routes];
10// '/' | '/about' | '/users'
11
12// Error if value doesn't match
13const badRoutes = {
14 home: '/',
15 about: 123, // Error: Type 'number' is not assignable to 'string'
16} as const satisfies Record<string, string>;Readonly Maps and Sets#
1// ReadonlyMap
2const userMap: ReadonlyMap<string, User> = new Map([
3 ['1', { id: 1, name: 'John' }],
4 ['2', { id: 2, name: 'Jane' }],
5]);
6
7userMap.get('1'); // OK
8userMap.set('3', user); // Error: Property 'set' does not exist
9
10// ReadonlySet
11const allowedRoles: ReadonlySet<string> = new Set([
12 'admin',
13 'user',
14 'guest',
15]);
16
17allowedRoles.has('admin'); // OK
18allowedRoles.add('new'); // ErrorMutable from Readonly#
1// Remove readonly modifier
2type Mutable<T> = {
3 -readonly [P in keyof T]: T[P];
4};
5
6interface ReadonlyUser {
7 readonly id: number;
8 readonly name: string;
9}
10
11type MutableUser = Mutable<ReadonlyUser>;
12// { id: number; name: string; }
13
14// Mutable array
15type MutableArray<T> = T extends readonly (infer U)[]
16 ? U[]
17 : T;
18
19type Numbers = MutableArray<readonly number[]>;
20// number[]Patterns and Use Cases#
1// Immutable state
2interface State {
3 readonly users: readonly User[];
4 readonly loading: boolean;
5 readonly error: string | null;
6}
7
8function reducer(state: State, action: Action): State {
9 switch (action.type) {
10 case 'ADD_USER':
11 return {
12 ...state,
13 users: [...state.users, action.payload],
14 };
15 default:
16 return state;
17 }
18}
19
20// Builder pattern with readonly result
21class ConfigBuilder {
22 private config: Partial<Config> = {};
23
24 setApiUrl(url: string): this {
25 this.config.apiUrl = url;
26 return this;
27 }
28
29 setTimeout(timeout: number): this {
30 this.config.timeout = timeout;
31 return this;
32 }
33
34 build(): Readonly<Config> {
35 return this.config as Config;
36 }
37}
38
39// Freeze deep objects
40function deepFreeze<T extends object>(obj: T): DeepReadonly<T> {
41 Object.keys(obj).forEach(key => {
42 const value = (obj as any)[key];
43 if (typeof value === 'object' && value !== null) {
44 deepFreeze(value);
45 }
46 });
47 return Object.freeze(obj) as DeepReadonly<T>;
48}Best Practices#
When to Use Readonly:
✓ Configuration objects
✓ Function parameters you won't modify
✓ Redux/state management
✓ Public API return types
When to Use as const:
✓ Object literal constants
✓ Array constants (enum-like)
✓ Preserving literal types
✓ Tuple creation
Patterns:
✓ Readonly for interfaces
✓ as const for values
✓ DeepReadonly for nested
✓ Mutable when needed
Avoid:
✗ Readonly on primitives (unnecessary)
✗ Over-using DeepReadonly
✗ Forgetting shallow nature
✗ Mixing mutable and readonly
Conclusion#
TypeScript's readonly modifiers and const assertions provide compile-time immutability guarantees. Use readonly for interface properties, Readonly<T> for entire types, and as const for literal value preservation. Remember that readonly is shallow by default - use DeepReadonly for nested immutability. These tools help prevent accidental mutations and make code intent clearer.