Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Утилітний тип readonly у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Readonly<T>** - це утилітний тип TypeScript, який робить усі властивості типу T незмінними на етапі компіляції. Читання працює вільно; будь-який запис стає помилкою компіляції. ```typescript interface Config { api: string; timeout: number; } const config: Readonly<Config> = { api: '/api', timeout: 5000 }; config.api = '/v2'; // Помилка: Cannot assign to 'api' ``` **Ключове:** `Readonly<T>` захищає тільки перший рівень - вкладені об'єкти залишаються змінюваними.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Readonly<T>** - це утилітний тип TypeScript, який робить кожну властивість типу T незмінною на етапі компіляції. Читати можна скільки завгодно. Записувати - ні. ## Теорія ### TL;DR - Уяви музейну вітрину: бачиш усе всередині, але нічого не можна переставити - `Readonly<T>` блокує тільки властивості верхнього рівня - вкладені об'єкти залишаються змінюваними - Саме ця поведінка з вкладеністю найчастіше дивує розробників - Використовуй, коли об'єкт передається між функціями і потрібен захист від випадкових мутацій на рівні компілятора - Жодного впливу на runtime - TypeScript видаляє це під час компіляції ### Швидкий приклад ```typescript interface User { id: number; name: string; } const user: Readonly<User> = { id: 1, name: 'Alice' }; console.log(user.name); // 'Alice' - читання працює нормально user.name = 'Bob'; // Error: Cannot assign to 'name' because it is a read-only property ``` TypeScript додає `readonly` до кожного ключа `User`. JavaScript-код при цьому не змінюється - жодних перевірок у рантаймі. ### Головна особливість: тільки перший рівень `Readonly<T>` блокує прямі властивості типу T. Якщо одна з властивостей сама є об'єктом, її вміст залишається змінюваним. Я бачив, як це спантеличувало досвідчених розробників при роботі з Redux-стейтом або вкладеними конфігами: мутація компілюється без помилок і залишається непоміченою до першого дивного бага в продакшені. ```typescript interface Settings { theme: string; db: { host: string }; } const settings: Readonly<Settings> = { theme: 'dark', db: { host: 'localhost' } }; // settings.theme = 'light'; // Помилка - верхній рівень заблокований settings.db.host = 'prod'; // Ок - вкладений об'єкт не захищений ``` ### Коли використовувати - Об'єкт передається через кілька функцій і побічні ефекти важко відстежити - Конфіг або налаштування, які задаються один раз при старті застосунку - Дані з API, які нижче по коду не повинні змінюватись - Пропси React-компоненту, щоб явно позначити контракт «не мутувати» ### Як це обробляє компілятор TypeScript розгортає `Readonly<T>` як відображуваний тип (mapped type): `{ readonly [K in keyof T]: T[K] }`. Компілятор проходить по кожному ключу T і додає модифікатор `readonly`. У JavaScript-код це не потрапляє. V8 цього ніколи не бачить. ### Типові помилки **1. Очікування глибокого захисту** ```typescript type Config = Readonly<{ db: { host: string } }>; const cfg: Config = { db: { host: 'localhost' } }; cfg.db.host = 'production'; // Компілюється без помилок ``` Виправлення: вкладай `Readonly` вручну або напиши рекурсивний mapped type: ```typescript type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> }; ``` **2. Масиви всередині Readonly приймають push** ```typescript type State = Readonly<{ items: number[] }>; const state: State = { items: [1, 2] }; state.items.push(3); // Дозволено - заблоковано посилання, але не сам масив ``` Виправлення: `Readonly<{ readonly items: readonly number[] }>`. **3. Індексні сигнатури залишаються відкритими** `Readonly<{ [k: string]: number }>` все одно дозволяє `obj['newKey'] = 1`. Обгортка не блокує додавання нових динамічних ключів. Щоб заблокувати і це, пиши модифікатор явно всередині: `{ readonly [k: string]: number }`. ### Де зустрічається - **React** - `function TodoItem(props: Readonly<TodoProps>)` запобігає мутаціям пропсів всередині компоненту; поширений патерн у Next.js 14+ - **Redux** - `type State = Readonly<{ todos: Todo[] }>` є в офіційних прикладах Redux Toolkit - **Серверні конфіги** - `type ServerConfig = Readonly<{ port: number; dbUrl: string }>` фіксує налаштування після старту - **Zod** - `z.object({...}).readonly()` автоматично повертає Readonly-версію виведеного типу ### Питання на співбесіді **Q:** Яка різниця між модифікатором `readonly` на властивості і `Readonly<T>`? **A:** `readonly id: number` захищає одну конкретну властивість. `Readonly<T>` охоплює всі властивості T одночасно. В JavaScript обидва варіанти дають однаковий результат - нічого. **Q:** Чи працює `Readonly<T>` з класами? **A:** Так, але тільки для публічних властивостей екземпляра. Приватні і захищені члени mapped type не зачіпає. Для класів зазвичай зручніше писати `readonly` прямо в тілі класу. **Q:** Як отримати повну (глибоку) незмінність? **A:** Напиши рекурсивний mapped type: `type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> }`. Бібліотека ts-toolbelt має готовий варіант. Immer дає глибоку незмінність на рівні runtime. **Q:** Чи є вплив на продуктивність? **A:** Жодного. TypeScript видаляє всі утилітні типи під час компіляції. Вихідний JavaScript однаковий незалежно від того, є Readonly чи ні. **Q:** Питання рівня senior - чому `Readonly<{ [K: string]: T }>` не блокує запис нових ключів? **A:** Readonly робить незмінними значення вже існуючих ключів, але не саму можливість додавати нові. Індексна сигнатура описує патерн для динамічних ключів, і mapped type wrapper цього не змінює. Щоб заблокувати обидві операції, пиши `{ readonly [K: string]: T }` без зовнішньої обгортки. ## Приклади ### Базовий: конфіг при старті застосунку ```typescript interface AppConfig { apiUrl: string; timeout: number; } const config: Readonly<AppConfig> = { apiUrl: 'https://api.example.com', timeout: 5000, }; // config.apiUrl = '/v2'; // Помилка: Cannot assign to 'apiUrl' console.log(config.apiUrl); // читання завжди працює ``` Конфіг задається один раз. Будь-який подальший запис - це помилка компіляції, а не тихий баг, який знайдеш тільки в рантаймі. ### Середній рівень: пропси React-компоненту ```typescript interface TodoProps { id: number; text: string; done: boolean; } function TodoItem({ id, text, done }: Readonly<TodoProps>) { // id = 42; // Помилка - деструктурований пропс тільки для читання return <li>{text} {done ? '(done)' : ''}</li>; } ``` Власні типи React вже обгортають пропси в `Readonly` внутрішньо. Явне оголошення додає ясності і ловить мутації під час розробки, до того як щось потрапить у браузер.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.