Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Утилітний тип pick у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Pick<T, K>** вибирає лише вказані ключі з T і будує з них новий тип із цими полями. ```ts interface User { id: number; name: string; email: string; } type UserPreview = Pick<User, 'id' | 'name'>; // Результат: { id: number; name: string } ``` **Ключове:** Pick працює тільки на рівні компіляції. Реальний JavaScript-об'єкт не змінюється, тому для фільтрації під час виконання потрібна ручна проекція.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Pick<T, K>** будує новий тип, вибираючи лише ті ключі, які ти назвав з T, зберігаючи їхні оригінальні типи і відкидаючи все інше. ## Теорія ### TL;DR - Уяви меню в ресторані: ти отримуєш рівно ті страви, які замовив, з повними рецептами, нічого зайвого. - `Pick<User, 'id' | 'name'>` дає `{ id: number; name: string }` — `email` та решта зникають. - Ключі мають існувати в T, інакше TypeScript видасть помилку при компіляції. Жодної фільтрації під час виконання — тільки перевірка типів. - Використовуй Pick для 1-5 пов'язаних пропсів. Якщо потрібно виключити багато ключів — краще підійде `Omit`. ### Швидкий приклад ```ts interface User { id: number; name: string; age: number; email: string; } // Вибираємо тільки 'id' і 'name' — age та email зникають type UserPreview = Pick<User, 'id' | 'name'>; const preview: UserPreview = { id: 1, name: 'Alice' }; // OK const invalid: UserPreview = { id: 1, name: 'Alice', age: 30 }; // Помилка ``` `UserPreview` знає лише про `id` і `name`. Передати `age` — помилка компіляції, а не виконання. ### Ключова різниця Pick створює **поверхневий підтип**. Він копіює точні типи з T без рекурсії у вкладені об'єкти. Якщо T має `{ address: { street: string } }`, то `Pick<T, 'address'>` зберігає весь вкладений об'єкт `address` повністю — просто відкидає інші ключі верхнього рівня. Обрізати вкладені поля одним викликом Pick не вийде. ### Коли використовувати - **Підмножина відповіді API**: показуй лише безпечні публічні поля з повної сутності (наприклад, прибери `passwordHash` перед відправкою клієнту). - **Пропси React-компонентів**: коли компонент потребує 2-4 поля з більшого доменного типу, Pick автоматично тримає пропси в синхронізації з вихідним типом. - **Форми**: DTO для часткового оновлення, де приймаєш лише конкретні поля. - **Payload подій**: вибери конкретні поля з типу тіла запиту. Якщо потрібно більше 5-6 ключів або є потреба трансформувати типи — краще підійде mapped type. ### Як TypeScript обробляє Pick всередині Pick визначений як mapped type: `type Pick<T, K extends keyof T> = { [P in K]: T[P] }`. Компілятор ітерується по K, шукає тип кожного ключа в T через indexed access і будує новий тип об'єкта. У скомпільованому JavaScript нічого не залишається — Pick повністю стирається після перевірки типів. Це означає нульову ціну під час виконання. JavaScript-код однаковий незалежно від того, чи анотуєш ти `User` або `Pick<User, 'id'>`. ### Типові помилки **Очікування фільтрації під час виконання** ```ts const fullUser = { id: 1, name: 'Alice', secret: 'pass123' }; const picked: Pick<User, 'id' | 'name'> = fullUser; // Компілюється! // Але secret все одно є в об'єкті під час виконання ``` Pick не видаляє властивості з реальних об'єктів. Він тільки обмежує те, що TypeScript дозволяє перевіряти. Щоб зробити проекцію під час виконання, будуй об'єкт вручну: `const picked = { id: fullUser.id, name: fullUser.name }`. **Широкий тип `string` як K** ```ts const key = 'id' as string; type Wrong = Pick<User, typeof key>; // Помилка: 'string' не сумісний з keyof User ``` K має бути рядковим літеральним типом. Пиши `'id' | 'name'` або підмножину `keyof User`, але не широкий `string`. **Опціональні пропси залишаються опціональними** ```ts interface User { id: number; name?: string; } type Picked = Pick<User, 'name'>; // { name?: string } — все одно опціональний ``` Якщо після Pick потрібно зробити `name` обов'язковим — огорни в `Required<Pick<User, 'name'>>`. **Вкладені типи потребують ланцюжка indexed access** ```ts interface User { details: { age: number }; } // Це дає { details: { age: number } } — не просто age type A = Pick<User, 'details'>; // Щоб отримати тільки age з details: type B = Pick<User['details'], 'age'>; // { age: number } ``` ### Де зустрічається в реальних проєктах - **React / Next.js**: `Pick<User, 'id' | 'name'>` як пропси компонента списку. - **Express / NestJS**: `Pick<User, 'name' | 'email'>` для DTO оновлення в контролерах. - **Redux Toolkit**: `Pick<Action, 'type' | 'payload'>` для типізованих action creators. - **tRPC**: вхідні дані процедур типізуються як `Pick<Entity, PublicKeys>`. Закономірність, яку я бачу в командах: анотують тип через Pick, але забувають проецювати об'єкт під час виконання. Чутливі поля потрапляють у відповідь, хоча TypeScript-тип виглядає правильно. ### Follow-up питання **Q:** Що поверне `Pick<User, keyof User>`? **A:** Той самий тип, що й `User`. Вибравши всі ключі, отримуєш вихідну форму без змін. **Q:** Чи можна використовувати Pick з union-типом? **A:** Так, `Pick<User | Admin, 'id' | 'name'>` розподіляється по union. Результат включає спільні ключі обох членів. **Q:** Що таке `Pick<T, never>`? **A:** Порожній тип об'єкта `{}`. Якщо не вибирати жодного ключа, отримуєш тип, який приймає тільки порожній об'єкт. **Q:** (Senior) Реалізуй Pick з нуля. **A:** `type MyPick<T, K extends keyof T> = { [P in K]: T[P] }`. Обмеження `K extends keyof T` — саме воно робить неіснуючі ключі помилкою компіляції. **Q:** Чим Pick відрізняється від ручного оголошення типу? **A:** Структури ідентичні, але Pick синхронізується автоматично. Якщо додаєш поле до `User` і воно є серед вибраних ключів, похідний тип оновлюється скрізь без ручних правок. ## Приклади ### Базовий: публічні поля User ```ts interface User { id: number; name: string; email: string; passwordHash: string; // це ніколи не відправляти isAdmin: boolean; } type PublicProfile = Pick<User, 'id' | 'name' | 'email'>; function getPublicProfile(user: User): PublicProfile { // Ручна проекція — Pick обмежує тільки тип, а не реальний об'єкт return { id: user.id, name: user.name, email: user.email, }; } ``` `passwordHash` і `isAdmin` виключені з типу повернення. Функція проецює явно — TypeScript не видаляє їх автоматично. ### Середній: пропси React-компонента з доменної моделі ```ts interface User { id: number; name: string; email: string; role: 'user' | 'admin'; createdAt: Date; } type UserCardProps = Pick<User, 'id' | 'name' | 'role'>; const UserCard = ({ id, name, role }: UserCardProps) => ( <div> {id} — {name} ({role}) </div> ); // Лише вибрані пропси обов'язкові <UserCard id={1} name='Bob' role='admin' />; ``` `UserCardProps` залишається в синхронізації з `User`. Якщо перейменуєш `role` в інтерфейсі, TypeScript відразу позначить кожен Pick, що посилається на нього. Це головна перевага перед ручним описом пропсів.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.