Практическое применение utility types
Внутренняя запись курса и запрос на её изменение имеют разное назначение. Полный Course содержит стабильный id, URL и все основные данные. Для обновления названия не нужно снова передавать весь объект, а разрешать произвольное изменение id было бы неверной политикой. Выделим формы создания и частичного обновления.
Utility types — готовые преобразования типов. В нашем случае Omit исключит поле, которое задаётся отдельно, Pick выберет разрешённые поля, а Partial сделает их необязательными. Получится небольшой интерфейс операций над уже известной моделью. Как и собственный mapped type, эти объявления не копируют данные и не проверяют внешние значения при выполнении.
Два назначения входных данных
CourseCreate описывает данные нового курса без идентификатора. В учебном примере id передаётся отдельным аргументом, чтобы его происхождение было явно отделено от остальных сведений. Такой выбор не является универсальным устройством API; в настоящем приложении идентификатор может назначать сервер или редакционная система.
CoursePatch включает только title и lessons, причём оба поля необязательны. Это означает «изменить только переданное», а не «разрешить любую неполную запись курса». Тема, URL и id в текущую операцию не входят. Выбранная форма показывает политику операции непосредственно в подписи.
Полный src/app.ts снимка lesson-13:
import { courses } from './data.js';
import type { Course } from './model.js';
type CourseCreate = Omit<Course, 'id'>;
type CoursePatch = Partial<Pick<Course, 'title' | 'lessons'>>;
function updateCourse(course: Course, patch: CoursePatch): Course {
const title = patch.title ?? course.title;
const lessons = patch.lessons ?? course.lessons;
if (title.trim() === '' || !Number.isInteger(lessons) || lessons <= 0) {
throw new Error('Некорректное обновление курса');
}
return { ...course, title, lessons };
}
function createCourse(id: string, input: CourseCreate): Course {
if (id.trim() === '') throw new Error('Пустой id');
return { ...input, id };
}
const first = courses[0];
if (first !== undefined) {
const updated = updateCourse(first, { title: 'JavaScript для каталога' });
const created = createCourse('javascript-copy', {
title: first.title, topic: first.topic, lessons: first.lessons, url: first.url
});
console.log(first.title);
console.log(updated.title);
console.log(created.id);
}
Ожидаются строки «Современный JavaScript», «JavaScript для каталога» и «javascript-copy». Исходный курс сохранит название, обновлённый объект получит новую подпись, а созданная учебная запись — отдельный id. Эти действия не добавляют записи в общий массив: мы рассматриваем значения результата.
Типы стандартных преобразований описаны в Utility Types. Здесь важно не запоминание всего справочника, а последовательность решения: сначала определяем допустимое действие, затем выбираем поля и выражаем их обязательность.
Частичность не ослабляет правила поля
В CoursePatch отсутствие title означает сохранение прежнего названия. Если поле есть, оно всё ещё строковое. Аналогично переданное количество должно быть числом. Настройка exactOptionalPropertyTypes помогает отличить отсутствие ключа от явно присвоенного undefined в обычном проверяемом исходнике.
При выполнении updateCourse дополнительно проверяет непустой текст и положительное целое число. Частичный тип не выражает эти смысловые ограничения. Поэтому значение { lessons: -1 } может соответствовать числовой форме, но операция всё равно обязана отказать. Нулевое число здесь не трактуется как отсутствие благодаря ??; оно попадает в проверку и признаётся неверным.
Мы намеренно читаем только два разрешённых поля и создаём результат { ...course, title, lessons }. Непроверенный дополнительный id в фактическом объекте patch не будет перенесён в результат через общий spread. Это поддерживает политику операции при выполнении, хотя внешний вход всё равно требует отдельной проверки его происхождения.
Если просто использовать { ...course, ...patch }, удобство записи может скрыть посторонние поля, полученные из JavaScript или JSON. Тип параметра ограничивает обычный исходный код, но не очищает внешний объект. Поэтому выбор явных полей здесь полезен не только для чтения сигнатуры.
Создание не означает регистрацию
createCourse соединяет input и отдельно переданный id. В конце spread находится id, поэтому именно аргумент функции определяет окончательный идентификатор. Мы проверяем его непустоту, но не проверяем уникальность среди каталога: функция не получает коллекцию и потому не имеет такого контекста.
Проверенные локальные данные создания предполагаются внутренним входом. Эта маленькая функция не является парсером внешнего API и не проверяет каждое поле заново. Если input пришёл из формы, сначала нужно преобразовать строки и проверить значения. Если он пришёл из JSON, начать следует с unknown. Нельзя выдавать CourseCreate за сертификат любого внешнего объекта.
Тот же URL у копии курса используется только для демонстрации формы. Функция не создаёт новый HTML-файл и не меняет маршрут сайта. Включённые courses/*.html остаются четырьмя учебными страницами. Для настоящего создания материала понадобился бы редакционный процесс записи файла, проверки адреса и регистрации записи.
Выбор Omit<Course, 'id'> сохраняет остальные модификаторы исходного типа. Необязательное описание остаётся необязательным, readonly-поля остаются договором чтения. Вход операции создания мы не изменяем на месте, поэтому изменяемость для него не требуется.
Пустое обновление и удаление значения
Partial разрешает пустой объект. В текущей операции updateCourse(course, {}) вернёт новый объект с теми же полями. Это допустимая политика, но вы вправе запретить бесполезное обновление отдельным правилом. Статическая частичность сама не требует хотя бы одного поля.
Рассмотрим самостоятельный фрагмент внутри проверки first:
const unchanged = updateCourse(first, {});
console.log(unchanged.title);
const moreLessons = updateCourse(first, { lessons: 24 });
console.log(moreLessons.lessons);
console.log(first.lessons);
Ожидаются прежнее название, 24 и 20. Последняя строка помогает увидеть, что функция создала новый объект вместо изменения исходного курса. Сумма общего каталога по-прежнему 60: новый результат не был записан в массив.
Удаление необязательного описания потребовало бы отдельного соглашения. Простое отсутствие поля в patch означает «не менять», поэтому оно не может одновременно означать «удалить». Можно выбрать явное действие очистки или специальный допустимый маркер. В этом уроке описание не входит в update-политику, и неоднозначность не скрывается универсальным Partial<Course>.
Массовое обновление нуждается в общей границе
В примере изменяется один курс. Если редактор обновляет несколько записей одновременно, индивидуальная корректность каждого результата не обеспечит уникальность всех id и согласованность общего каталога. Для такой операции нужно проверить коллекцию и определить, когда изменения считаются подтверждёнными.
У нашего CoursePatch id вообще не доступен для изменения. Это уменьшает риск разрыва ссылок в памяти, но не проверяет существование файла по URL и не обновляет внешние маршруты. Поэтому демонстрацию нового объекта нельзя объявить миграцией опубликованной статьи. Название результата изменилось, исходный источник остался прежним.
Сохранение прежнего количества при отсутствующем поле отличается от сохранения нуля. Ноль присутствует и проходит до проверки положительности, где получает отказ. Это одна из причин выбрать ?? вместо ||: логическое выражение могло бы незаметно заменить неверный ноль прежним корректным количеством и скрыть проблему входа.
Порядок spread тоже связан с политикой. В createCourse отдельный id находится последним и не может быть заменён полем из input при обычном выполнении. В updateCourse последние title и lessons являются уже проверенными выбранными значениями. Код не просто собирает удобную строку: он показывает, какие сведения получают приоритет и какие поля разрешены операции. Для последующих изменений сохраняйте эту причинную связь между типом входа, проверкой значения и порядком создания результата.
Когда форма операций начинает значительно отличаться от сохранённой записи, отдельные явные интерфейсы бывают понятнее цепочки преобразований. Utility types полезны для устойчивых связей, но не обязывают строить каждую бизнес-модель исключительно из Course. Решение определяется смыслом действий и читаемостью договора.
Исходники при подготовке не исполнялись. Наблюдаемые результаты описаны как ожидаемые, а создание нового значения не объявлено созданием опубликованной страницы. В следующем уроке рассмотрим конечные действия интерфейса: фильтр и сортировку, точные литералы и проверку конфигурации без потери её конкретных значений.