Skip to main content

Утилітарний тип partial у TypeScript

Partial<T> - вбудований утилітарний тип TypeScript, який перетворює кожну властивість об'єктного типу T на необов'язкову.

Теорія

TL;DR

  • Уяви чернетку: заповнюєш тільки поля, які вже є, решту пропускаєш
  • { name: string } стає { name?: string } для кожної властивості T
  • Підходить для PATCH-запитів, стану форм і конфігураційних об'єктів
  • Працює лише на першому рівні. Вкладені об'єкти залишаються обов'язковими, якщо їх передати
  • Жодного впливу на runtime. TypeScript видаляє тип під час компіляції

Швидкий приклад

ts
type User = { id: number; name: string; email: string; }; // Всі поля стають необов'язковими const update: Partial<User> = { name: "Alice" }; // id та email пропущені - ок // Патерн злиття const fullUser: User = { id: 1, name: "Bob", email: "bob@test.com" }; const updated: User = { ...fullUser, ...update }; // { id: 1, name: "Alice", email: "bob@test.com" }

update містить лише поле, яке треба змінити. Spread об'єднує його з повним User.

Головна відмінність

Partial<T> масштабується разом з T автоматично. Додаєш нове поле до User - кожен Partial<User> у кодовій базі відображає це без жодних змін. Зручніше ніж вручну підтримувати { name?: string; email?: string } і стежити за синхронізацією.

Коли використовувати

  • PATCH-запити до API: надсилаєш серверу лише змінені поля
  • Стан форми в React: відстежуєш, які поля користувач вже заповнив
  • Конфігураційні об'єкти: задаєш дефолти і дозволяєш перевизначати частину
  • Оновлення в reducer: зливаєш частковий payload з поточним станом

Як компілятор обробляє це

TypeScript розгортає Partial<T> як mapped type: { [K in keyof T]?: T[K] }. Компілятор проходить кожен ключ T і додає ?. У JavaScript нічого не потрапляє. V8 і браузер про цей тип не знають.

Типові помилки

Помилка 1: очікування рекурсивної роботи Partial.

ts
type Nested = { user: { name: string; age: number }; }; const x: Partial<Nested> = { user: { name: "Alice" } }; // Помилка! age все ще обов'язковий

Partial торкається лише верхнього рівня. Якщо передаєш user, його внутрішня структура залишається повністю обов'язковою. Ця пастка спрацьовує майже завжди вперше. Для глибокої опціональності потрібен власний DeepPartial:

ts
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;

Помилка 2: забуті перевірки на undefined в runtime.

ts
function update(data: Partial<User>) { console.log(data.id.toString()); // Компілюється. Падає якщо id відсутній. }

TypeScript не генерує жодних перевірок. Додавай сам: if (data.id !== undefined).

Помилка 3: використовувати Partial коли одне поле завжди обов'язкове.

Якщо id завжди required, Partial<User> дозволяє його пропустити теж. Комбінуй типи:

ts
type PatchUser = Pick<User, "id"> & Partial<Omit<User, "id">>;

Помилка 4: застосовувати Partial до примітиву.

ts
type ID = number; const x: Partial<ID> = undefined; // Помилка. Partial<number> залишається number.

Примітиви не мають ключів. Partial на них не впливає. Використовуй number | undefined.

Де зустрічається

  • React (React Hook Form): useState<Partial<Profile>>({}) для часткового стану форми
  • NestJS: Partial<CreateUserDto> як тип body для PATCH-ендпоінтів
  • Redux Toolkit: Partial<State> в slice reducers для опціонального оновлення полів
  • Prisma: prisma.user.update({ data: partial }) приймає часткову форму моделі

Питання на співбесіді

Q: У що розгортається Partial<{ a: string; b: number }>?
A: У { a?: string; b?: number }. Кожна властивість отримує ?.

Q: Partial зберігає модифікатор readonly?
A: Ні. Partial<{ readonly x: number }> прибирає readonly, залишаючи { x?: number } як записуване.

Q: Чи є вплив на runtime-продуктивність?
A: Жодного. Це чистий compile-time тип. JavaScript-вивід однаковий з Partial і без нього.

Q: Яка різниця між Partial<T> і { [K in keyof T]?: T[K] }?
A: Вони ідентичні. Partial<T> - іменований псевдонім цього mapped type, визначений у lib.es5.d.ts.

Q: (Senior) Як описати тип для PATCH-ендпоінту, де id завжди обов'язковий, а решта полів - ні?
A: type PatchUser = Pick<User, 'id'> & Partial<Omit<User, 'id'>>. Це гарантує наявність id і робить решту опціональними.

Приклади

Базовий: оновлення об'єкта через spread

ts
type Point = { x: number; y: number; z?: number }; function move(origin: Point, delta: Partial<Point>): Point { return { ...origin, ...delta }; } const start: Point = { x: 0, y: 0 }; const result = move(start, { x: 10 }); // { x: 10, y: 0 }

delta містить лише координати, які треба змінити. move об'єднує їх з поточною позицією і повертає повний Point.

Проміжний: стан форми в React

ts
interface Profile { name: string; age: number; bio: string; } function EditProfileForm({ user }: { user: Profile }) { const [draft, setDraft] = useState<Partial<Profile>>({}); const handleSave = () => { const updated: Profile = { ...user, ...draft }; // безпечне злиття saveProfile(updated); }; return ( <input value={draft.name ?? user.name} onChange={(e) => setDraft((d) => ({ ...d, name: e.target.value }))} /> ); }

draft зберігає лише поля, яких торкнувся користувач. При збереженні вони зливаються з оригінальним user, і жодне поле не зникає. Патерн працює з будь-якою бібліотекою форм або звичайним React state.

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

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

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

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