Suggest an editImprove this articleRefine the answer for “Utility type readonly in TypeScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Readonly<T>** is a TypeScript utility type that makes all properties of T non-reassignable at compile time. Reads still work; writes become a compile error. ```typescript interface Config { api: string; timeout: number; } const config: Readonly<Config> = { api: '/api', timeout: 5000 }; config.api = '/v2'; // Error: Cannot assign to 'api' because it is a read-only property ``` **Key:** `Readonly<T>` is shallow - nested objects stay mutable.Shown above the full answer for quick recall.Answer (EN)Image**Readonly<T>** is a TypeScript utility type that makes every property of T non-reassignable at compile time. Reads work freely. Writes become a compile error. ## Theory ### TL;DR - Think of a museum display case: you can see everything inside but can't move any piece - `Readonly<T>` applies shallow immutability - only top-level properties get locked - Nested objects stay fully mutable; this is what catches most developers off guard - Use it when an object passes between functions and you want compile-time protection against accidental writes - Zero runtime cost - TypeScript erases it before the code runs ### Quick example ```typescript interface User { id: number; name: string; } const user: Readonly<User> = { id: 1, name: 'Alice' }; console.log(user.name); // 'Alice' - reads work fine user.name = 'Bob'; // Error: Cannot assign to 'name' because it is a read-only property ``` TypeScript adds `readonly` to every key of `User`. The emitted JavaScript is identical to a plain object assignment - no runtime guards, no overhead. ### Key difference: shallow, not deep `Readonly<T>` locks direct properties of T. If a property holds an object, that inner object stays mutable. In practice, I've seen this bite developers most often when wrapping a Redux slice or a nested config - the write compiles cleanly and the mutation goes unnoticed until something breaks at runtime. ```typescript interface Settings { theme: string; db: { host: string }; } const settings: Readonly<Settings> = { theme: 'dark', db: { host: 'localhost' } }; // settings.theme = 'light'; // Error - top-level is blocked settings.db.host = 'prod'; // Fine - nested object is not protected ``` ### When to use - Object passed across multiple functions where accidental side effects would be hard to trace - Config or settings created at startup that should never change - API response data that should stay read-only downstream - React component props to make the no-mutation contract explicit ### How the compiler handles this TypeScript expands `Readonly<T>` as a mapped type: `{ readonly [K in keyof T]: T[K] }`. It walks each key of T and adds the `readonly` modifier. Nothing changes in the emitted JavaScript. V8 and Node never see it. ### Common mistakes **1. Assuming nested objects are also protected** ```typescript type Config = Readonly<{ db: { host: string } }>; const cfg: Config = { db: { host: 'localhost' } }; cfg.db.host = 'production'; // Compiles. No error. ``` Fix: nest `Readonly` explicitly or write a recursive mapped type: ```typescript type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> }; ``` **2. Arrays inside Readonly still accept push** ```typescript type State = Readonly<{ items: number[] }>; const state: State = { items: [1, 2] }; state.items.push(3); // Allowed - the reference is locked, not the array itself ``` Fix: `Readonly<{ readonly items: readonly number[] }>`. **3. Index signatures stay open** `Readonly<{ [k: string]: number }>` still allows `obj['newKey'] = 1`. The wrapper doesn't seal dynamic key addition. Write the modifier inline instead: `{ readonly [k: string]: number }`. ### Real-world usage - **React** - `function TodoItem(props: Readonly<TodoProps>)` prevents in-component props mutation; common in Next.js 14+ components - **Redux** - `type State = Readonly<{ todos: Todo[] }>` appears in official Redux Toolkit examples - **Server config** - `type ServerConfig = Readonly<{ port: number; dbUrl: string }>` ensures setup objects don't drift after startup - **Zod** - `z.object({...}).readonly()` produces a Readonly output type from a schema automatically ### Follow-up questions **Q:** What is the difference between the `readonly` modifier on a property and `Readonly<T>`? **A:** `readonly id: number` targets one specific property. `Readonly<T>` applies to every property of T at once. Both produce identical JavaScript output - nothing. **Q:** Does `Readonly<T>` work with classes? **A:** Yes, for public instance properties. Private and protected members are not affected by the mapped type. For classes, adding `readonly` directly in the class body is more precise. **Q:** How do you get true deep immutability? **A:** Write a recursive mapped type: `type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> }`. Libraries like ts-toolbelt ship one out of the box. Immer handles deep immutability at the runtime level. **Q:** Any runtime cost? **A:** None. TypeScript erases all type constructs during compilation. The output is the same with or without Readonly. **Q:** Senior question - why does `Readonly<{ [K: string]: T }>` not prevent writing new keys? **A:** Readonly locks the values at existing keys, not the act of adding new ones. An index signature describes a pattern for dynamic keys, and the mapped type wrapper doesn't change that behavior. Write `{ readonly [K: string]: T }` inline to block both reassignment of existing values and addition of new keys. ## Examples ### Basic: config object at startup ```typescript interface AppConfig { apiUrl: string; timeout: number; } const config: Readonly<AppConfig> = { apiUrl: 'https://api.example.com', timeout: 5000, }; // config.apiUrl = '/v2'; // Error: Cannot assign to 'apiUrl' console.log(config.apiUrl); // reads always work ``` Config is set once at startup. Any later write is a compile error, not a silent runtime bug discovered in production. ### Intermediate: React component props ```typescript interface TodoProps { id: number; text: string; done: boolean; } function TodoItem({ id, text, done }: Readonly<TodoProps>) { // id = 42; // Error - the destructured binding is read-only return <li>{text} {done ? '(done)' : ''}</li>; } ``` React's own typings already wrap props in `Readonly` internally. Doing it explicitly here signals intent to anyone reading the code and catches mutations during development, before anything reaches the browser.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.