Skip to main content

Утилітний тип pick у TypeScript

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, що посилається на нього. Це головна перевага перед ручним описом пропсів.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?