Utility types: Partial, Pick, Omit
«У нас есть тип User с пятью полями. Нужна функция обновления, куда передают только часть полей — и отдельная форма регистрации, где не нужен id. Ты же не будешь писать под каждый случай отдельный тип вручную?» — спрашивает интервьюер, проверяя, знаешь ли ты utility types.
Utility types (вспомогательные типы) — это встроенные в TypeScript дженерики, которые берут существующий тип и производят из него новый: делают поля необязательными, выбирают часть полей, исключают часть полей и так далее — без ручного переписывания структуры.
Вопрос-крючок
Вот тип и наивный способ сделать «версию для обновления», где не все поля обязательны:
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Наивно: копипаста всего типа вручную, только с ? у каждого поля
interface UserUpdateManual {
id?: number;
name?: string;
email?: string;
age?: number;
}
function updateUser(id: number, changes: UserUpdateManual): void {
console.log("Обновляю пользователя", id, "полями:", changes);
}
updateUser(1, { name: "Аня Новая" });Вывод:
Обновляю пользователя 1 полями: { name: 'Аня Новая' }Код работает, но есть проблема, которая всплывёт не сразу: если завтра в <code>User</code> добавят поле <code>phone</code>, про <code>UserUpdateManual</code> придётся вспомнить и обновить его отдельно, вручную. Про такие ручные копии легко забыть — и тогда типы начинают расходиться с реальностью. Именно для этого в TypeScript есть готовый инструмент.
Partial: делает все поля необязательными
<code>Partial<T></code> берёт тип T и возвращает такой же тип, но где каждое поле помечено как необязательное (<code>?</code>). Это ровно то, что мы только что написали руками для UserUpdateManual — только теперь оно выводится автоматически и всегда синхронизировано с оригиналом:
interface User {
id: number;
name: string;
email: string;
age: number;
}
function updateUser(id: number, changes: Partial<User>): void {
console.log("Обновляю пользователя", id, "полями:", changes);
}
updateUser(1, { name: "Аня Новая" });
updateUser(2, { age: 21, email: "boris@example.com" });
// updateUser(3, {}); // тоже допустимо — все поля Partial необязательныВывод:
Обновляю пользователя 1 полями: { name: 'Аня Новая' }
Обновляю пользователя 2 полями: { age: 21, email: 'boris@example.com' }Если завтра в <code>User</code> добавят поле <code>phone: string</code>, <code>Partial<User></code> автоматически подхватит его как необязательное — никакого второго места для правки не нужно.
Pick: выбирает только нужные поля
<code>Pick<T, Keys></code> берёт тип T и оставляет из него только перечисленные поля — остальные выбрасывает. Это удобно, когда нужен «срез» большого типа для конкретной задачи, например для короткой карточки пользователя в списке:
interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserPreview = Pick<User, "id" | "name">;
// Эквивалентно: { id: number; name: string }
const preview: UserPreview = { id: 1, name: "Аня" };
console.log(preview);
// const bad: UserPreview = { id: 1, name: "Аня", email: "a@a.com" };
// ошибка: email лишний, его не было в списке PickВывод:
{ id: 1, name: 'Аня' }Второй параметр — <code>"id" | "name"</code> — это union из строковых литералов, буквально имена полей, которые нужно оставить. TypeScript проверит, что каждое перечисленное имя реально существует в исходном типе <code>User</code> — опечатку в названии поля поймает компилятор, а не тестировщик.
Omit: исключает ненужные поля
<code>Omit<T, Keys></code> делает обратное — оставляет всё, <strong>кроме</strong> перечисленных полей. Классический случай — форма регистрации, где ещё нет <code>id</code> (его выдаст сервер после создания записи):
interface User {
id: number;
name: string;
email: string;
age: number;
}
type RegistrationForm = Omit<User, "id">;
// Эквивалентно: { name: string; email: string; age: number }
function register(data: RegistrationForm): User {
return { id: Date.now(), ...data };
}
const newUser = register({ name: "Ева", email: "eva@example.com", age: 19 });
console.log(newUser.name, newUser.email, typeof newUser.id);Вывод:
Ева eva@example.com number<code>Omit<User, "id"></code> и <code>Pick<User, "name" | "email" | "age"></code> в этом конкретном случае дают одинаковый результат — но Omit удобнее, когда исключаемых полей мало, а оставшихся много: не нужно перечислять всё, что остаётся.
Зачем не писать эти типы вручную каждый раз
Дело не только в экономии печати. Главная причина — синхронизация с источником. Ручная копия типа (как <code>UserUpdateManual</code> из первого примера) начинает жить своей жизнью: кто-то добавит поле в <code>User</code>, забудет про копию — и типы разъедутся, а компилятор об этом не предупредит, ведь с его точки зрения оба типа объявлены корректно, просто по отдельности. <code>Partial<User></code>, <code>Pick<User, ...></code> и <code>Omit<User, ...></code>, наоборот, <strong>всегда</strong> выводятся заново из актуального <code>User</code> — рассинхронизация просто невозможна структурно.
Как это работает под капотом
Partial, Pick и Omit — не какая-то магия компилятора, а обычные встроенные дженерик-типы, объявленные в стандартной библиотеке типов TypeScript через специальные конструкции (mapped types — «отображаемые типы», которые перебирают поля исходного типа и переписывают их по правилу). Их можно было бы написать и самому — именно так они и устроены внутри:
| Utility type | Что делает | Похоже на ручной аналог |
Partial<T> | Все поля становятся необязательными (?) | Переписать T, добавив ? к каждому полю |
Pick<T, K> | Оставляет только поля из K | Переписать T, выбросив все поля, кроме перечисленных |
Omit<T, K> | Оставляет всё, кроме полей из K | Переписать T, выбросив перечисленные поля |
Как и все дженерики, utility types работают только на этапе компиляции. В скомпилированном JavaScript ни <code>Partial</code>, ни <code>Pick</code>, ни <code>Omit</code> не существуют — объекты в рантайме остаются обычными JS-объектами, а вся работа этих типов — это подсказки компилятору и редактору, какая форма объекта ожидается в конкретном месте кода.
Частые ошибки на собеседовании
- Путают Pick и Omit местами. Pick — «оставь только эти поля», Omit — «оставь всё, кроме этих полей». Проще запомнить по смыслу английских слов: pick — выбрать, omit — пропустить.
- Пишут вручную копию типа вместо utility type — и не замечают, что при изменении оригинального типа копия перестаёт совпадать, а компилятор этого не покажет: оба типа по отдельности корректны, просто рассинхронизированы.
- Забывают, что второй параметр Pick и Omit — это union строковых литералов с именами полей, а не тип этих полей. Опечатка в имени поля (например, "naem" вместо "name") — это ошибка компиляции, потому что такого литерала нет среди ключей исходного типа.
- Считают Partial глубоким (deep). На самом деле Partial делает необязательными только поля <strong>верхнего уровня</strong>. Если внутри User есть вложенный объект address с обязательными полями, Partial<User> не сделает поля внутри address необязательными — только само поле address целиком.
Итоги
- Utility types — встроенные в TypeScript дженерики, которые производят новый тип из существующего: Partial, Pick, Omit и другие.
- Partial<T> делает все поля T необязательными — типичный случай для функций частичного обновления.
- Pick<T, Keys> оставляет только перечисленные поля, Omit<T, Keys> — все, кроме перечисленных; Keys задаётся как union строковых литералов с именами полей.
- Главное преимущество перед ручной копией типа — автоматическая синхронизация: при изменении исходного типа производный тип пересчитывается сам, рассинхронизация невозможна.
- Как и все дженерики, utility types существуют только на этапе компиляции и не оставляют следа в скомпилированном JavaScript.