Загружаем научный разбор
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Автор: Казачкин Даниил Михайлович · Обновлено
Проверяем границы преобразований моделей: почему Omit не удаляет данные, Partial меняет состояния команды, Record зависит от ключей, а union может потерять связи.
В большом приложении исходная сущность редко совпадает с данными, которые нужны каждому потребителю. Редактор меняет только часть полей, карточка показывает краткое описание, обработчик команды требует ограниченный набор значений. Utility types позволяют выразить эти отношения без повторного перечисления всей структуры. Но сокращение кода полезно лишь тогда, когда преобразование сохраняет нужный смысл. Компилируемая модель может незаметно разрешить пустую команду, потерять связь между вариантами или скрыть поле только от редактора, оставив его в отправляемом объекте.
Разберём преобразования как небольшую алгебру контрактов. Это авторское объяснение с проверками компилятора и исполнения, а не эмпирическое сравнение качества проектов. Синтаксис основных утилит и начальные упражнения находятся в [прикладном уроке об utility types](/lessons/typescript/typescript-for-developers/typescript-utility-types). Здесь нас интересует, какое утверждение появляется после каждого преобразования и какое более сильное утверждение из него ошибочно выводят.
Первый уровень — описание допустимых обращений в TypeScript. Второй — фактический объект JavaScript со своими свойствами. Третий — предметное правило: например, публичная карточка не должна содержать внутреннюю заметку. Преобразование типа непосредственно работает на первом уровне. Для согласования остальных уровней нужны реализация и проверяемый контракт.
Pick выбирает свойства модели, Omit исключает названные свойства, Partial делает свойства необязательными, а Record описывает соответствие ключей значениям. Это определения формы типа, а не команды изменения объекта. TypeScript: Utility Types.
Возьмём собственную модель учебной публикации. Все последующие примеры можно проверять установленным компилятором с strict, exactOptionalPropertyTypes и noUncheckedIndexedAccess. Версию компилятора фиксируйте в отчёте: расширенные флаги являются частью условий опыта, а не подразумеваются любым проектом с TypeScript.
type Publication = {
id: string;
title: string;
internalNote: string;
schedule: { opensAt: string; closesAt: string };
};
type Preview = Pick<Publication, 'id' | 'title'>;
type PublicShape = Omit<Publication, 'internalNote'>;
type EditTitle = Partial<Pick<Publication, 'title'>>;У Preview есть положительное правило выбора: перечислено, что потребитель использует. У PublicShape есть отрицательное правило: перечислено, чего в видимой форме нет. При добавлении нового поля к Publication второй тип автоматически подхватит его, первый останется прежним. Это различие важно при ревью схемы: удобная автоматическая синхронизация может оказаться нежелательным расширением публичного контракта.
Проверим гипотезу «после присваивания типу Omit исключённое поле исчезает». Нам нужен объект, который уже существует, и две независимые проверки: принимает ли его компилятор и какое значение получает сериализатор.
const publication: Publication = {
id: 'intro',
title: 'Начало',
internalNote: 'Требует проверки',
schedule: { opensAt: '09:00', closesAt: '18:00' },
};
const visible: PublicShape = publication;
console.log(JSON.stringify(visible).includes('internalNote')); // true
function toPreview(value: Publication): Preview {
return { id: value.id, title: value.title };
}
console.log(JSON.stringify(toPreview(publication))); // {"id":"intro","title":"Начало"}Первое присваивание разрешено структурными правилами: исходный объект содержит необходимые поля. Во время исполнения оно сохраняет ссылку на тот же объект. Второй результат получается потому, что функция действительно создаёт объект с выбранными свойствами. Тип результата помогает проверить код функции, но саму работу выполняет объектный литерал. TypeScript: Type Compatibility.
Из опыта следует конкретное правило проектирования публичного ответа. Сначала определите список передаваемых данных, затем реализуйте проекцию и проверьте сериализованный результат. Если использовать spread всего исходного объекта, новые внутренние поля способны пройти через границу незаметно для автора прежнего преобразования. Утилита Omit остаётся полезной для описания формы, но проверка конфиденциальности требует наблюдения фактических данных.
На первый взгляд Partial<Publication> выглядит естественной моделью частичного обновления. Однако такой тип разрешает пустой объект и предлагает менять идентификатор вместе с остальными полями. Более узкая модель Partial<Pick<Publication, 'title'>> выражает другую обязанность: единственное доступное изменение относится к заголовку. Требование «хотя бы одно поле» всё равно остаётся отдельным.
Изменение необязательности можно представить как расширение набора допустимых форм. Раньше команда обязана была содержать заголовок; теперь допускается отсутствие этого поля. Вопрос «что означает отсутствие?» относится к протоколу приложения. Это может быть «оставить прежнее значение», «применить значение по умолчанию» или недопустимая операция. Компилятор не выбирает такую семантику за разработчика.
Partial преобразует свойства рассматриваемого уровня. Если передано поле schedule, его внутренние обязательные поля сохраняют свои требования. Механизм выражается через mapped type, который проходит по ключам модели и меняет модификатор свойства. Из этой формы не следует рекурсивный обход вложенных объектов. TypeScript: Mapped Types.
const emptyEdit: EditTitle = {};
// @ts-expect-error: вложенное closesAt остаётся обязательным
const incompleteSchedule: Partial<Publication> = { schedule: { opensAt: '10:00' } };Ошибка второго объявления полезна, если расписание должно обновляться целиком. Если протокол допускает отдельное изменение времени открытия, нужна другая модель команды. Универсальный DeepPartial тоже не решит предметный вопрос: как трактовать массивы, даты, функции и удаление вложенного значения? До написания рекурсивной утилиты полезнее описать два конкретных запроса, которые сервер должен различать.
Пусть {} означает отсутствие изменения, а { title: undefined } случайно возникло после чтения другого объекта. При включённом exactOptionalPropertyTypes необязательное свойство без явного undefined не принимает второй вариант. Флаг позволяет сохранить различие между отсутствием свойства и присутствующим значением undefined. TSConfig: exactOptionalPropertyTypes.
const unchanged: EditTitle = {};
// @ts-expect-error: требуется отсутствие поля либо строка
const ambiguous: EditTitle = { title: undefined };
console.log('title' in unchanged); // falseНельзя, однако, сделать вывод, что транспорт автоматически сохранит все такие различия. Например, сериализация JSON и обработчик обновления имеют собственное поведение. Для команды очистки поля часто разумно выделить явное значение или отдельную операцию. Тогда тест может проверить конкретную пару «запрос — изменение», а не спорить, какой смысл имел undefined в промежуточной переменной.
Record<'draft' | 'published', string> описывает конечную таблицу: для обоих известных ключей нужны строки. Record<string, string> описывает обращения по произвольному строковому ключу, но не создаёт бесконечный объект. Это заметно при обращении к ключу, которого фактически нет.
const labels: Record<'draft' | 'published', string> = {
draft: 'Черновик',
published: 'Опубликовано',
};
const dictionary: Record<string, string> = {};
const absent = dictionary['missing']; // string | undefined с выбранным флагом
console.log(labels.draft, absent); // Черновик undefinednoUncheckedIndexedAccess добавляет возможность отсутствия значения при подобных непроверенных индексных обращениях. Он не меняет содержимое словаря; меняется обязанность вызывающего кода обработать неопределённость. TSConfig: noUncheckedIndexedAccess.
При проектировании конфигурации сначала установите, закрыт ли набор ключей. Для известных режимов конечный union позволяет заметить пропущенную ветку при добавлении нового режима. Для произвольных пользовательских идентификаторов отсутствие записи является нормальным состоянием, и API должен выразить его честно. Одно и то же слово Record не делает эти два договора одинаковыми.
Теперь проверим более тонкую гипотезу: «если убрать общее поле у union, все остальные связи сохранятся». Возьмём состояние публикации с обязательным автором проверки только после выпуска. Нас интересует именно зависимость между признаком и полезной нагрузкой.
type ReviewState =
| { kind: 'draft'; id: string; notes: string[] }
| { kind: 'published'; id: string; reviewedBy: string };
type Collapsed = Omit<ReviewState, 'id'>;
const tooLittle: Collapsed = { kind: 'published' }; // принимается
type RemoveId<T> = T extends unknown ? Omit<T, 'id'> : never;
type Preserved = RemoveId<ReviewState>;
// @ts-expect-error: у опубликованного варианта нужен reviewedBy
const rejected: Preserved = { kind: 'published' };Обычный Omit здесь не выполняет независимое преобразование каждого варианта так, как ожидал автор гипотезы. Условный тип с проверяемым параметром типа распределяет операцию по составляющим union. Это правило распределения описано отдельно от самих utility types. TypeScript: Conditional Types.
Практический смысл опыта — проверять сохранение отношений, а не только список названий свойств. Для модели состояния недостаточно увидеть в подсказке kind. Нужно убедиться, что каждому его значению соответствует нужная нагрузка. Минимальная пара допустимого и недопустимого значения часто обнаруживает проблему быстрее длинной цепочки вложенных утилит.
Есть ещё один контроль, который трудно увидеть на исходном удачном примере: измените саму сущность. Добавьте в Publication поле с внутренней категорией, которое не должно попадать в карточку. Предскажите, какие производные типы изменятся автоматически. Затем добавьте третий вариант ReviewState и проверьте преобразование без id. Такие изменения имитируют обычную эволюцию приложения и показывают, чьё решение наследует утилита: автора общей сущности или автора конкретного представления.
Для команды редактирования полезно сделать обратный опыт: уберите поле из основной модели. Производный тип с явно выбранным ключом должен обратить внимание на разрыв связи, а отдельная вручную скопированная структура способна продолжить существовать незаметно. Это объясняет ценность производных типов без обещания, что любая зависимость всегда полезна. Правильная связь должна срабатывать именно при том изменении, которое требует пересмотра потребителя.
Запишите ожидаемые последствия рядом с контрактом: новое внутреннее поле не расширяет публичный ответ; новый вариант состояния требует пересмотра обработки; удалённое редактируемое поле обнаруживается при сборке. Такая формулировка даёт будущему ревьюеру критерий оценки.
При автоматизированной проверке 8 сентября 2026 года объединённый комплект примеров прошёл TypeScript 5.9.3 и 6.0.3 с указанными тремя флагами. Помеченные отрицательные случаи действительно вызвали диагностику; запуск подтвердил сохранение internalNote в первом объекте и отсутствие поля в явной проекции. Это проверка конкретных контрактов примера, а не доказательство безопасности любого преобразования с теми же утилитами.
Перед добавлением утилиты сформулируйте три контрольных примера: обычный допустимый объект, намеренно недопустимый объект и пограничный случай. Для публичной проекции дополнительно проверьте сериализацию. Для команды обновления — пустое изменение и явную очистку. Для union — каждый вариант без обязательной нагрузки. Так проверяется содержание контракта, а не способность TypeScript напечатать красивый тип.
В учебном проекте можно сделать редактор расписания и сравнить две команды: полная замена расписания и изменение одного времени. Выберите модели, напишите обработчики и объясните, почему одни и те же входные данные допустимы для одной команды и недостаточны для другой. [Урок о mapped types](/lessons/typescript/typescript-for-developers/typescript-mapped-types) поможет выразить преобразование, [урок об условных типах](/lessons/typescript/typescript-for-developers/typescript-conditional-types) — сохранить варианты, а [урок о runtime-валидации](/lessons/typescript/typescript-for-developers/typescript-runtime-validation) — проверить данные на входе. Полезная утилита делает правило видимым и проверяемым для следующего разработчика.
Авторский русскоязычный разбор. Перечисленные первичные источники использованы для проверки технических утверждений и библиографии; материал не является переводом или перепечаткой источников. Собственные примеры и рассуждения отделены от опубликованных эмпирических результатов.
Иллюстрации, таблицы и код первоисточников не воспроизводятся. Примеры и текстовые схемы созданы для этой статьи. Ссылки и библиографические сведения не предоставляют прав на перепубликацию материалов источников.