Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке маповані типи в TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Mapped types** (маповані типи) створюють новий тип, перебираючи кожен ключ існуючого типу і застосовуючи трансформацію до типу його значення. ```typescript type Flags<T> = { [K in keyof T]: boolean }; type User = { name: string; age: number }; type UserFlags = Flags<User>; // { name: boolean; age: boolean } ``` **Ключове:** `[K in keyof T]` перебирає union ключів типу `T`; додавай `?` або `readonly` для модифікаторів, `as` для перейменування ключів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Mapped types** (маповані типи) - TypeScript-конструкція, яка створює новий тип, перебираючи кожен ключ існуючого типу і застосовуючи трансформацію до типу його значення. ## Теорія ### TL;DR - Аналог `Array.prototype.map()` для типів об'єктів: вхід `{name: string, age: number}`, вихід `{name: boolean, age: boolean}` після зміни типу кожного значення - Базовий синтаксис: `{ [K in keyof T]: NewValueType }` перебирає всі ключі `T` - `?` робить поля optional, `readonly` заморожує їх, `-readonly` і `-?` прибирають модифікатори - Key remapping через `as` (TypeScript 4.1+): фільтрація або перейменування ключів через template literals - Використовуй коли вбудовані утиліти (`Partial<T>`, `Pick<T, K>`) не покривають потрібну трансформацію ### Швидкий приклад ```typescript type User = { name: string; age: number; }; // Замапити кожне поле на boolean type BooleanUser = { [K in keyof User]: boolean; }; // Результат: { name: boolean; age: boolean } const perms: BooleanUser = { name: true, age: false }; // ok ``` Синтаксис `[K in keyof User]` перебирає union ключів типу `User` і застосовує `boolean` як тип значення для кожного ключа. TypeScript генерує нову форму об'єкта при перевірці типів. У рантаймі нічого не виконується. ### Головна різниця Mapped types трансформують кожне поле систематично. Простий type alias лише перейменовує структуру, не чіпаючи її. Перетин (intersection) додає нові поля зверху. Mapped types реагують на джерельний тип: додай поле до `User` і `BooleanUser` підхопить його автоматично. Саме ця реактивність робить їх корисними для generic-утиліт. ### Синтаксис модифікаторів Чотири патерни покривають більшість випадків: - `[K in keyof T]?: T[K]` робить всі поля optional, аналог `Partial<T>` - `readonly [K in keyof T]: T[K]` робить всі поля readonly, аналог `Readonly<T>` - `[K in keyof T]-?: T[K]` прибирає optional з усіх полів, аналог `Required<T>` - `-readonly [K in keyof T]: T[K]` прибирає readonly, корисний як хелпер `Mutable<T>` Префікс `-` в TypeScript означає "прибрати модифікатор". Модифікатори можна комбінувати з трансформацією значень в одному маппінгу. ### Key remapping через `as` (TypeScript 4.1+) Key remapping дозволяє перейменовувати або фільтрувати ключі під час маппінгу: ```typescript type EventHandlers<T> = { [K in keyof T as K extends `on${string}` ? K : never]: T[K]; }; interface ButtonProps { onClick: () => void; label: string; disabled?: boolean; } type Handlers = EventHandlers<ButtonProps>; // Результат: { onClick: () => void } ``` Клауза `as` запускає conditional для кожного ключа. Повертай `never` щоб прибрати ключ з результату. Повертай template literal type щоб перейменувати. Цей патерн часто зустрічається в бібліотеках компонентів, де потрібно відокремити event callbacks від DOM-атрибутів. ### Гомоморфний маппінг Mapped type є гомоморфним, коли він маппиться безпосередньо по `keyof T`. У цьому випадку TypeScript зберігає оригінальні модифікатори `readonly` і `?` зі вхідного типу, якщо ти явно не перевизначаєш їх. ```typescript type User = { readonly id: string; name?: string }; type Copy = { [K in keyof User]: User[K] }; // Результат: { readonly id: string; name?: string } - модифікатори збереглись ``` Маппінги по довільних union, наприклад `[K in string]`, гомоморфними не є і модифікатори не зберігають. Це часте джерело плутанини при роботі з index signatures. ### Як TypeScript обробляє mapped types Компілятор розкриває mapped type під час перевірки типів, підставляючи кожен ключ з union в клаузу маппінгу. Для `[K in keyof T]` він проходить по union ключів `T` і генерує новий літерал об'єктного типу для кожної підстановки. Runtime-коду не генерується: маповані типи стираються при компіляції так само, як і всі інші анотації типів. ### Типові помилки **Помилка 1: очікування, що index signature збережеться після маппінгу** ```typescript type Obj = { [key: string]: number }; type Bad = { [K in keyof Obj]?: Obj[K] }; // keyof Obj резолвиться в string | number // але index signature з результату зникає ``` Рішення: перетни результат маппінгу з index signature вручну: ```typescript type Good = { [K in keyof Obj]?: Obj[K] } & { [key: string]: number | undefined }; ``` **Помилка 2: випадкове прибирання `readonly` з вхідного типу** ```typescript type RUser = { readonly id: string }; type Bad = { [K in keyof RUser]: string }; // id: string - readonly зник, бо явний тип значення перевизначив його ``` Щоб навмисно прибрати `readonly`, використовуй `-readonly`. Щоб зберегти - використовуй `RUser[K]` як тип значення і покладайся на гомоморфне збереження. **Помилка 3: вкладені маппінги без рекурсії** ```typescript // Не працює для вкладених об'єктів - T[K] не трансформується рекурсивно type ShallowPartial<T> = { [K in keyof T]?: Partial<T[K]> }; ``` Рішення - рекурсивний conditional type: ```typescript type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T; ``` **Помилка 4: спроба перейменувати ключі без клаузи `as`** ```typescript // Тут можна змінити лише тип значення, не ключ type Bad<T> = { [K in keyof T]: string }; // Правильно: використовуй `as` з Capitalize або template literals type Good<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: T[K] }; ``` ### Де зустрічається - **TypeScript stdlib:** `Partial<T>`, `Required<T>`, `Readonly<T>`, `Pick<T, K>` і `Record<K, V>` - все маповані типи під капотом - **React:** `React.ComponentProps<typeof Button>` використовує mapped types для витягування типів HTML-атрибутів з компонента - **TanStack Query:** `UseQueryOptions` маппить ключі на optional через `{ [K in keyof T]?: T[K] }` - **tRPC:** маппить форми входу і виходу процедур з модифікаторами для кожного endpoint - **Zod:** `.partial()` на схемі використовує той самий патерн з модифікатором `?` всередині ### Питання на співбесіді **Q:** Яка різниця між mapped types і conditional types? **A:** Mapped types ітеруються по ключах для побудови форми об'єкта. Conditional types (`T extends U ? A : B`) розгалужуються за відношеннями між типами. Вони добре комбінуються: `{ [K in keyof T]: T[K] extends string ? Uppercase<T[K]> : T[K] }` використовує обидва механізми в одному виразі. **Q:** Що таке гомоморфний mapped type? **A:** Маппінг безпосередньо по `keyof T`. Він зберігає оригінальні модифікатори `readonly` і `?` з типу `T`, якщо ти явно не перевизначаєш їх. Маппінги по довільних union, наприклад `[K in string]`, гомоморфними не є. **Q:** Як побудувати глибокий `Partial`? **A:** Через рекурсивний conditional type: `type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;`. Conditional зупиняє рекурсію на примітивах. **Q:** Чи можна перейменовувати ключі за допомогою template literal types? **A:** Так, з TypeScript 4.1. Клауза `as` приймає будь-який тип-вираз, що резолвиться в `string | number | symbol | never`, включно з template literal types на кшталт `` `on${Capitalize<string & K>}` ``. **Q:** Реалізуй `Diff<T, U>` що прибирає з `T` ключі, присутні в `U`, зберігаючи оригінальні модифікатори. **A:** `type Diff<T, U extends keyof any> = { [K in keyof T as K extends U ? never : K]: T[K] };`. Key remapping з conditional маппить виключені ключі в `never`, і TypeScript прибирає їх з результату. Оскільки маппінг іде по `keyof T` безпосередньо, він залишається гомоморфним і зберігає модифікатори. ## Приклади ### Базовий: трансформація типів значень ```typescript type Status = { isActive: boolean; isAdmin: boolean; isVerified: boolean; }; // Замінити всі boolean на string-мітки type StatusLabels = { [K in keyof Status]: string; }; // Результат: { isActive: string; isAdmin: string; isVerified: string } ``` Форма `StatusLabels` повністю повторює `Status`, але тип кожного значення замінено. Додай нове поле до `Status` і `StatusLabels` підхопить його автоматично. ### Проміжний: витягування обробників подій з React props ```typescript interface FormProps { onSubmit: (e: Event) => void; onChange: (value: string) => void; placeholder: string; disabled?: boolean; } // Залишити лише ключі, що починаються з "on" type HandlerProps<T> = { [K in keyof T as K extends `on${string}` ? K : never]: T[K]; }; type FormHandlers = HandlerProps<FormProps>; // Результат: { onSubmit: (e: Event) => void; onChange: (value: string) => void } ``` Клауза `as K extends \`on${string}\` ? K : never` фільтрує набір ключів. Будь-який ключ, що не відповідає патерну, маппиться в `never` і зникає з результату. Той самий механізм лежить в основі `React.ComponentProps`. ### Senior: перейменування ключів з умовним виключенням ```typescript type User = { name: string; age: number; readonly id: string; }; // Перейменувати `age` на `viewAge`, прибрати `id` з результату type SecureUser = { readonly [K in keyof User as K extends 'id' ? never : K extends 'age' ? 'viewAge' : K ]: User[K]; }; // Результат: { readonly name: string; readonly viewAge: number } ``` Модифікатор `readonly` на маппінгу застосовується до всіх вихідних ключів. Поле `id` зникає, бо його ключ маппиться в `never` в клаузі `as`. Бачив цей патерн в трансформерах API-відповідей, де серверні назви полів потрібно змінювати перед передачею в UI, і перевірка на рівні типів відловлює невідповідності ще на етапі компіляції.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.