Utility types: Pick, Omit, Partial и производные модели без копирования полей
Автор: Казачкин Даниил Михайлович · Обновлено
Utility types позволяют получить новое описание из уже существующего типа: выбрать поля, исключить служебные свойства, сделать часть данных необязательной или извлечь результат…
Utility types позволяют получить новое описание из уже существующего типа: выбрать поля, исключить служебные свойства, сделать часть данных необязательной или извлечь результат функции. Их главное практическое преимущество — сохранение связи между моделями, которые должны меняться вместе.
Цель и предпосылки
Нужны объектные типы и базовое понимание generics из предыдущего урока. Мы построим модели редактора учебных курсов, отдельно разберём Omit и проверим, почему преобразование типа не очищает объект во время выполнения. Пример рассчитан на TypeScript 6 с strict и не использует внешних библиотек.
Выбор полей вместо второго списка типов
Внутренняя запись курса содержит идентификатор, заголовок, длительность, дату изменения и служебную заметку. Для публичной карточки нужны только первые три поля. Если создать независимый интерфейс и повторить их типы, изменение идентификатора придётся находить в нескольких местах. Pick описывает более точную договорённость: эта карточка использует перечисленные свойства именно исходной модели.
export {};
interface CourseRecord {
id: string;
title: string;
minutes: number;
updatedAt: string;
internalNote: string;
}
type CourseCard = Pick<CourseRecord, 'id' | 'title' | 'minutes'>;
type EditableCourse = Omit<CourseRecord, 'id' | 'updatedAt' | 'internalNote'>;
type CoursePatch = Partial<EditableCourse>;
const course: CourseRecord = {
id: 'course-ts', title: 'Типы', minutes: 90,
updatedAt: '2026-09-08', internalNote: 'Проверить примеры',
};
const typedView: CourseCard = course;
const projected: CourseCard = {
id: course.id, title: course.title, minutes: course.minutes,
};
const patch: CoursePatch = { minutes: 120 };
console.log('internalNote' in typedView);
console.log('internalNote' in projected);
console.log(JSON.stringify({ ...projected, ...patch }));
if (false) {
// @ts-expect-error Редактор не должен менять служебный идентификатор.
const wrongPatch: CoursePatch = { id: 'another-course' };
void wrongPatch;
}Вывод: true, затем false, затем {"id":"course-ts","title":"Типы","minutes":120}. Первая переменная предоставляет более узкое описание той же ссылки: служебное свойство физически осталось на объекте. Во втором случае создан новый объект с выбранными полями. Именно выражение JavaScript выполнило проекцию данных; аннотация проверила, что эта проекция соответствует ожидаемой форме.
Omit исключает свойства из типа
Omit<T, K> удобно применять, когда новая модель действительно означает «все поля исходной, кроме перечисленных». Например, служебные поля базы не должны входить в форму редактирования. Но у такого решения есть следствие: новое поле исходной модели автоматически попадёт в производную, если не входит в исключения. Для публичного ответа часто надёжнее явно перечислять разрешённые поля через Pick, потому что список раскрываемых данных должен быть осознанным.
Не используйте Omit как замену очистке перед отправкой. Проверка на избыточные свойства особенно полезна для свежего литерала, но не превращает структурную типизацию в точное удаление всех лишних полей. Если важно проверить фактический ответ, сравните его ключи или сериализованное представление. Такая проверка ловит ошибку, которую невозможно устранить одним переименованием типа.
Partial, Required и Readonly не действуют глубоко
Partial делает свойства верхнего уровня необязательными. Если в модели есть вложенный settings, его отсутствие может быть разрешено, но присутствующий объект по-прежнему должен соответствовать собственному типу. Required меняет обязательность свойств, а не создаёт недостающие значения. Readonly ограничивает записи через соответствующее представление типа, но не вызывает замораживание объекта и не делает автоматически неизменяемыми все вложенные ссылки.
Для обновлений отдельно определите семантику отсутствия, null и явного undefined. Например, отсутствие заголовка может означать «сохранить старый», а пустая строка — быть ошибкой. Точная допустимость явного undefined в необязательном поле зависит в том числе от exactOptionalPropertyTypes. Даже строгая настройка не решает, что делать с полем после получения JSON: это правило операции обновления, которое необходимо реализовать.
Извлечение информации из функций и объединений
Parameters и ReturnType полезны, когда нужно связать адаптер с сигнатурой существующей функции. Awaited моделирует разворачивание результата асинхронной операции; комбинация Awaited<ReturnType<typeof loadCourse>> позволяет получить тип успешного значения без ещё одного ручного списка полей. Не путайте это с ожиданием обещания во время выполнения: само ожидание делает await в обычном коде.
Extract и Exclude работают с членами объединений, а не удаляют поля объекта. Для удаления свойства используется Omit; для выбора состояния с подходящей меткой можно использовать Extract. В частности, Exclude<number, 0> не создаёт практически проверяемый тип всех ненулевых чисел. Если число должно быть ненулевым, нужна проверка значения или специально поддерживаемая модель такого ограничения.
Типичные ошибки
Не применяйте utility type только ради сокращения количества символов. Иногда новая сущность похожа на старую случайно и должна развиваться независимо. Не объявляйте все команды обновления как Partial огромного DTO: это может разрешить изменение полей, которые пользователь редактировать не должен. Не считайте встроенный Omit автоматически распределяющимся по каждой ветви сложного union; при необходимости сохранения ветвей сначала проверьте результат на небольшом примере и изучите условные типы.
Практикум: редактирование без служебных полей
Расширьте CourseRecord полями published: boolean и ownerId: string. Редактору разрешены только заголовок, длительность и признак публикации. Создайте тип изменения и функцию применения к существующей записи. Для длительности допустимы положительные конечные числа; заголовок после обрезки пробелов не должен быть пустым. Проверьте корректное изменение, попытку подменить владельца и нулевую длительность.
Разбор: удобен Partial<Pick<CourseRecord, 'title' | 'minutes' | 'published'>>, потому что список разрешённых изменений явный. Ограничения значений проверяются обычным кодом до объединения объектов. Результат сохраняет id, ownerId и другие служебные поля исходной записи. Проверка запрещённого поля литерала подтверждает типовой договор, а тест неверной длительности — предметное правило. Если команда пришла извне, до этой функции требуется проверка её фактических полей: TypeScript не присутствует в сетевом сообщении.
Частые вопросы
Pick и Omit нужны только для интерфейсов?
Нет. Им важен подходящий объектный тип, а не способ его объявления. Можно преобразовывать псевдоним, интерфейс или тип, полученный из другого выражения. Выбирайте исходник, который действительно является источником истины для производной модели.
Когда лучше написать отдельный тип вручную?
Когда совпадение полей не означает общую ответственность за изменения. Например, публичный договор сторонней системы не должен незаметно меняться вслед за внутренней формой. Производные типы полезны для намеренной связи, но не заменяют решение о границах моделей.
Связанные исследования
- Utility types как преобразования моделей: что доказывают Pick, Omit, Partial и Record — Проверяем границы преобразований моделей: почему Omit не удаляет данные, Partial меняет состояния команды, Record зависит от ключей, а union может потерять связи.