Утилітарний тип partial у TypeScript
Partial<T> - вбудований утилітарний тип TypeScript, який перетворює кожну властивість об'єктного типу T на необов'язкову.
Теорія
TL;DR
- Уяви чернетку: заповнюєш тільки поля, які вже є, решту пропускаєш
{ name: string }стає{ name?: string }для кожної властивості T- Підходить для PATCH-запитів, стану форм і конфігураційних об'єктів
- Працює лише на першому рівні. Вкладені об'єкти залишаються обов'язковими, якщо їх передати
- Жодного впливу на runtime. TypeScript видаляє тип під час компіляції
Швидкий приклад
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.
type Nested = {
user: { name: string; age: number };
};
const x: Partial<Nested> = { user: { name: "Alice" } }; // Помилка! age все ще обов'язковийPartial торкається лише верхнього рівня. Якщо передаєш user, його внутрішня структура залишається повністю обов'язковою. Ця пастка спрацьовує майже завжди вперше. Для глибокої опціональності потрібен власний DeepPartial:
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;Помилка 2: забуті перевірки на undefined в runtime.
function update(data: Partial<User>) {
console.log(data.id.toString()); // Компілюється. Падає якщо id відсутній.
}TypeScript не генерує жодних перевірок. Додавай сам: if (data.id !== undefined).
Помилка 3: використовувати Partial коли одне поле завжди обов'язкове.
Якщо id завжди required, Partial<User> дозволяє його пропустити теж. Комбінуй типи:
type PatchUser = Pick<User, "id"> & Partial<Omit<User, "id">>;Помилка 4: застосовувати Partial до примітиву.
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
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
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.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.