Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Індекси підписів та типи доступу до індексів у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Індексна сигнатура (index signature)** у TypeScript задає тип для об'єктів з довільними рядковими або числовими ключами через синтаксис `[key: string]: T`. Тип доступу за індексом (`T[K]`) читає тип конкретної властивості з наявного типу. ```typescript interface Dict { [key: string]: number } // індексна сигнатура type User = { id: string; name: string }; type UserId = User["id"]; // string - тип доступу type AllValues = User[keyof User]; // string ``` **Головне:** сигнатура описує об'єкт для запису в рантаймі; тип доступу - це виключно компайл-тайм операція без жодного коду на виході.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Індексна сигнатура (index signature)** описує тип для об'єктів з довільними рядковими або числовими ключами. Тип доступу за індексом (`T[K]`) читає тип конкретної властивості з наявного типу на рівні компілятора. Це два різні інструменти для двох різних задач. ## Теорія ### TL;DR - Індексна сигнатура (`[key: string]: T`) = контракт на структуру об'єкта з довільними ключами - Тип доступу за індексом (`T["key"]`) = зчитування типу властивості з іншого типу - Аналогія: сигнатура - це правило готелю (будь-яка картка відкриває номер одного класу); тип доступу - це читання категорії номера з плану поверху - Сигнатура впливає на те, що можна записати в рантаймі; тип доступу не генерує жодного JS-коду - `Record<string, T>` і `[key: string]: T` структурно ідентичні; `Record` коротший і зрозуміліший ### Короткий приклад ```typescript // Індексна сигнатура: будь-який рядковий ключ відображається на рядок interface StringDict { [key: string]: string; } const dict: StringDict = { a: "apple" }; dict["b"] = "banana"; // OK // Тип доступу за індексом: зчитує тип ключа "a" type Fruit = { a: "apple"; b: "banana" }; type AppleType = Fruit["a"]; // "apple" type AllValues = Fruit[keyof Fruit]; // "apple" | "banana" ``` Сигнатури дозволяють записувати динамічні ключі в рантаймі. Типи доступу розв'язуються під час перевірки типів і повністю зникають зі скомпільованого JavaScript. ### Головна різниця Індексна сигнатура визначає контракт: кожне значення, збережене під будь-яким ключем, повинно відповідати задекларованому типу, і TypeScript перевіряє це під час присвоєння. Тип доступу за індексом нічого не робить у рантаймі. `User["email"]` - це операція на рівні типів, яка зводиться до типу поля `email` в `User`. Їх можна ланцюжити: `User["address"]["city"]` заглиблюється на два рівні у вкладений тип без жодного звернення до значення в рантаймі. ### Коли що використовувати - Конфігураційні об'єкти з ключами, які задає користувач: `[key: string]: unknown` - Словники рядок-значення: `Record<string, T>` (читабельніше за сиру сигнатуру) - Вилучення типу властивості для обобщеної функції: `T[K]` - Отримання всіх типів значень з інтерфейсу: `T[keyof T]` - Типізація елементів масиву `as const`: `typeof arr[number]` - Об'єкти тільки з відомими властивостями: сигнатури не потрібні взагалі ### Як це обробляє компілятор TypeScript застосовує перевірку зайвих властивостей до свіжих об'єктних літералів. Якщо присвоїти `{ a: 1, b: "oops" }` типу `[key: string]: number`, компілятор відразу видасть помилку, бо `"oops"` не є числом. Після присвоєння через змінну (структурна перевірка) TypeScript розширює невідомі ключі до типу сигнатури без скарг. Типи доступу розв'язуються через type mapper: `T[K]` підставляється під час constraint solving без генерації коду. Обидві конструкції повністю стираються в JS-виводі. З практики: якщо поєднуєш відомі властивості зі строковою індексною сигнатурою, всі явні поля мусять задовольняти тип сигнатури. Це часто призводить до розширення до `string | number | undefined`, тому команди зазвичай використовують `Record` для чистих словників, а окремі інтерфейси для структурованих даних. ### Типові помилки **Відомі властивості конфліктують з індексною сигнатурою** ```typescript interface Config { name: string; version: number; // Помилка: number не присвоюється до string [key: string]: string; } ``` Кожна явна властивість повинна задовольняти сигнатуру. Рішення - розширити тип: ```typescript interface Config { name: string; version: number; [key: string]: string | number; // тепер сумісно } ``` **Використання `T[string]` замість `T[keyof T]`** ```typescript type Obj = { a: 1; b: 2 }; type Bad = Obj[string]; // Помилка: 'string' не є допустимим типом індексу type Good = Obj[keyof Obj]; // 1 | 2 ``` `string` занадто широкий. Використовуй `keyof Obj` або конкретний union літералів. **Запис у `readonly` сигнатуру** ```typescript interface ReadDict { readonly [key: string]: string; } const d: ReadDict = { a: "ok" }; d["b"] = "no"; // Помилка: індексна сигнатура тільки для читання ``` Прибери `readonly`, якщо потрібна мутація. Залишай лише якщо незмінюваність задумана навмисно. **Забуваєш `as const` при доступі до елементів масиву** ```typescript const roles = ["admin", "editor"]; // string[] type Role = typeof roles[number]; // string (занадто широко) const rolesConst = ["admin", "editor"] as const; type RoleConst = typeof rolesConst[number]; // "admin" | "editor" ``` Без `as const` TypeScript виводить `string[]`, і тип доступу повертає просто `string` замість union літералів. ### Де зустрічається в реальному коді - React: `React.CSSProperties[keyof React.CSSProperties]` дає всі можливі типи значень стилів - Express: параметри маршруту типізовані як `Record<string, string>` в `req.params` - Redux: `Action["payload"]` вилучає тип корисного навантаження для конкретного action - Lodash: `Dictionary<T>` - це `[key: string]: T` під капотом - Node.js: `process.env` типізований як `{ [key: string]: string | undefined }` ### Питання на співбесіді **Q:** Яка різниця між `[key: string]: T` і `Record<string, T>`? **A:** Структурно ідентичні. `Record<string, T>` розгортається в ту саму індексну сигнатуру. Більшість команд обирає `Record` через зрозумілість синтаксису. **Q:** Чому `keyof` строково-індексованого інтерфейсу повертає `string | number`? **A:** JavaScript перетворює числові ключі в рядки всередині, тому TypeScript додає `number`, щоб врахувати, що `obj[0]` і `obj["0"]` - одне й те саме. **Q:** Чи можна ланцюжити типи доступу за індексом? **A:** Так. `User["address"]["city"]` спочатку розв'язує `address`, потім шукає `city` в отриманому типі. Працює на будь-якій глибині, доки кожний крок є валідним ключем. **Q:** Як працює `typeof arr[number]` з масивами `as const`? **A:** `as const` перетворює масив у readonly кортеж з літеральними типами елементів. Індексація через `number` дає union усіх цих літеральних типів. **Q:** Напиши типобезпечну функцію `get` з використанням типу доступу за індексом. **A:** ```typescript function get<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { id: "1", name: "Alice" }; const name = get(user, "name"); // виводиться як string ``` `K extends keyof T` обмежує ключі до допустимих, а `T[K]` зберігає точний тип повернення без жодного приведення. ## Приклади ### Базовий: типізований словник ```typescript interface Translations { [key: string]: string; } const i18n: Translations = { hello: "привіт", goodbye: "до побачення", }; const greeting = i18n["hello"]; // тип: string, значення: "привіт" const missing = i18n["nope"]; // тип: string, реальне значення: undefined ``` TypeScript не знає, які ключі існують у рантаймі. Якщо хочеш щоб тип відображав можливий `undefined`, зміни сигнатуру на `[key: string]: string | undefined`. ### Середній: типізація ендпоінтів API ```typescript interface User { id: string; name: string } interface Post { id: number; title: string } interface ApiResponses { "/users": { users: User[] }; "/posts": { posts: Post[] }; } async function fetchData<T extends keyof ApiResponses>( endpoint: T ): Promise<ApiResponses[T]> { const res = await fetch(endpoint); return res.json(); } const data = await fetchData("/users"); // TypeScript виводить: data - це { users: User[] } // Зміни ендпоінт на "/posts" і тип повернення зміниться автоматично ``` Тип доступу `ApiResponses[T]` прив'язує тип повернення до аргументу-ендпоінту. Жодного приведення типів не потрібно. ### Просунутий: вилучення вкладених типів ```typescript interface Theme { colors: { primary: string; secondary: string; }; spacing: { sm: number; md: number; lg: number; }; } type ThemeColors = Theme["colors"]; // { primary: string; secondary: string } type SpacingKeys = keyof Theme["spacing"]; // "sm" | "md" | "lg" type SpacingValue = Theme["spacing"][SpacingKeys]; // number // Обобщений хелпер для будь-якої секції type Section<T, K extends keyof T> = T[K]; type ColorSection = Section<Theme, "colors">; // те саме що Theme["colors"] ``` Ланцюжки типів доступу дозволяють уникнути дублювання визначень інтерфейсів. У коді дизайн-систем цей патерн зустрічається постійно при побудові типізованих утиліт для теми.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.