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.
Проверьте себя
1. В чём главный риск ручного дублирования типа (например, отдельный интерфейс UserUpdateManual с теми же полями, но через ?) вместо использования Partial<User>?
AРучная копия работает медленнее в рантайме
BTypeScript вообще запрещает дублировать типы вручную
CПри изменении исходного типа ручная копия не обновится автоматически, а компилятор не предупредит о рассинхронизации
DРучные копии типов нельзя использовать в функциях
2. Тип User имеет поля id, name, email, age. Чему будет эквивалентен Omit<User, "id" | "age">?
A{ id: number; age: number }
B{ name: string; email: string }
C{ id?: number; age?: number }
DПустому типу {}