TypeScript / 38 минут
Utility types и неизменяемость
Используйте Pick, Omit, Partial и Record на границах данных.
- Используйте Pick, Omit, Partial и Record на границах данных.
- Применить тему в задаче «Utility types и неизменяемость» и объяснить граничные случаи.
Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.
Производные формы вместо ручного дублирования
Один объект приложения может участвовать в нескольких контрактах: полная запись, данные создания, частичное обновление и публичный ответ. Если переписывать каждую форму вручную, поля постепенно расходятся. Utility types помогают выразить связь между типами и сделать её проверяемой. Они преобразуют статическое описание, но не преобразуют реальные объекты автоматически.
Выбирайте производный тип по назначению конкретного контракта. Не делайте один гигантский интерфейс с optional у каждого поля только для того, чтобы он подходил всем стадиям. Слишком слабая модель позволяет создавать бессмысленные состояния. Иногда отдельный явный DTO лучше, чем сложная цепочка преобразований, особенно когда публичная форма должна оставаться стабильной при изменении внутреннего хранения.
Partial, Required и Readonly
Partial<T> делает свойства необязательными. Это удобно для ограниченного обновления, но не означает, что любое пустое изменение имеет смысл для бизнеса. Required<T> делает свойства обязательными на уровне типа. Readonly<T> запрещает запись через данное представление, но работает поверхностно и не выполняет Object.freeze во время исполнения.
type User = { id: string; name: string; active: boolean };
type UserPatch = Partial<Pick<User, 'name' | 'active'>>;
type UserView = Readonly<User>;В этом UserPatch нельзя менять id, хотя исходный User его содержит. Это лучше отражает разрешённое действие, чем Partial<User> без ограничений. Сервер всё равно должен проверить неизвестный JSON и авторизацию изменения. Наличие статического UserPatch в файле не удаляет присланное злоумышленником поле role из HTTP-body.
Pick и Omit
Pick выбирает указанные поля, Omit исключает перечисленные. Для публичного ответа часто безопаснее перечислить разрешённые свойства через Pick, чем исключить один известный секрет через Omit. Если во внутреннюю запись позже добавят другое приватное поле, отрицательный список может случайно начать считать его публичным.
type StoredUser = { id: number; name: string; password: string };
type PublicUser = Pick<StoredUser, 'id' | 'name'>;
function publicUser(user: StoredUser): PublicUser {
return { id: user.id, name: user.name };
}Ключевой шаг здесь — создание нового объекта с разрешёнными полями. Возврат исходного user под аннотацией PublicUser не удалил бы password во время исполнения. Это одна из причин, почему проверка отсутствия приватных полей в HTTP-тесте важнее красивого типа результата сама по себе.
Record и словари
Record<K,V> описывает отображение ключей K в значения V. Если K — закрытый union, тип может требовать все перечисленные ключи. Если K — произвольный string, фактическое наличие каждого возможного ключа в runtime не гарантировано. Проверка отсутствия остаётся важной частью чтения словаря, особенно для внешних или неполных данных.
type Status = 'loading' | 'ready' | 'error';
const labels: Record<Status, string> = {
loading: 'Загрузка', ready: 'Готово', error: 'Ошибка',
};Такая таблица помогает заметить добавленный статус, для которого забыли подпись. Но не превращайте каждый объект сущности в Record<string,unknown>, если потребителю нужны конкретные обязательные свойства. Структура типа должна сохранять знание о данных, а не стирать его ради универсальности.
ReturnType, Parameters, Awaited и ограничения
ReturnType извлекает возвращаемый тип функции, Parameters — кортеж параметров, Awaited моделирует получение результата ожидаемого значения. Эти операции полезны для связанных адаптеров и вспомогательных API. Однако бесконечное выведение публичного контракта из внутренней реализации может сделать случайное изменение helper незаметным изменением большого внешнего API.
async function loadCount(): Promise<number> { return 22; }
type Pending = ReturnType<typeof loadCount>; // Promise<number>
type Count = Awaited<Pending>; // number
type Args = Parameters<(id: string, limit: number) => void>;Exclude и Extract работают с вариантами union, NonNullable исключает null и undefined. Они не проверяют реальное значение в runtime. Например, NonNullable<T> в типе не доказывает, что внешняя функция действительно не вернула null. Для такого доказательства нужна ветвь проверки или другой реальный контракт.
Практика публичного ответа
В задании из объекта с id, name и password нужно вернуть новый объект только с id и name. Проверьте отсутствие password в фактическом результате, а не только соответствие TypeScript-аннотации. Не используйте spread исходного объекта: он перенесёт секретное поле. Это упражнение связывает преобразование типа с осмысленным runtime-проецированием.
После решения добавьте во вход вымышленное новое приватное поле и обсудите, какая модель публичного ответа останется устойчивой. Затем опишите UserPatch, запрещающий менять идентификатор. Для каждой utility-операции проговорите две отдельные вещи: какую статическую связь она выражает и какое обычное JavaScript-действие всё ещё должен выполнить код.