Сегодня нас ждет один из тех уроков, после которого вы почувствуете настоящую силу системы типов TypeScript. Мы будем говорить о утилитах типов (Utility Types).
До этого мы часто создавали типы вручную, описывая каждое свойство. Но представьте, что у вас есть большой тип User и вам вдруг понадобился точно такой же тип, но чтобы все его поля были необязательными. Или тип, который состоит только из двух конкретных свойств User. Неужели каждый раз придется городить новый интерфейс или тип? Конечно же, нет! Для этих целей в TypeScript есть встроенные хелперы, утилиты типов. Это такие инструменты, которые позволяют на лету преобразовывать существующие типы, создавая на их основе новые. Это механизм для повторного использования и комбинации типов.
В этом уроке мы детально разберем три самые популярные и часто используемые утилиты: Partial, Pick и Omit. Мы поймем, какую проблему каждая из них решает, посмотрим на синтаксис и закрепим знания множеством практических примеров.
Что такое утилиты типов?
Утилиты типов это встроенные в TypeScript обобщенные (generic) типы, которые принимают другой тип в качестве параметра и возвращают новый, преобразованный тип. Звучит немного сложно, но на практике все очень просто. Представьте себе функцию, которая на вход получает какой-то объект (например, строку), а на выходе выдает преобразованный результат (например, эту же строку, но в верхнем регистре). Утилиты типов, это такие же «функции», но работают они на уровне системы типов, а не на уровне значений.
Их ключевая польза, в соблюдении принципа DRY (Don’t Repeat Yourself — «Не повторяйся»). Вместо того чтобы создавать новые типы с нуля и дублировать код, мы можем взять уже существующий тип и модифицировать его под наши нужды с помощью утилит. Это делает код более чистым, легче поддерживаемым и менее подверженным ошибкам. TypeScript предоставляет из коробки довольно много таких утилит и сегодня мы фокусируемся на трех фундаментальных.
Утилита Partial<T>
Давайте начнем с, пожалуй, самой востребованной утилиты, это Partial<T>. Ее название говорит само за себя: «частичный» или «неполный». Задача Partial создать новый тип, в котором все свойства исходного типа T становятся необязательными (?).
Эта утилита невероятно полезна в ситуациях, когда вы работаете с объектами, которые могут обновляться частично. Классический пример, форма редактирования профиля пользователя. У вас есть тип User с множеством полей: имя, почта, пароль, адрес и т.д. Но пользователь может захотеть изменить только свой email, не трогая все остальное. Функция обновления профиля в таком случае должна принимать объект, где любое из полей User может быть, а может и не быть. Именно здесь на помощь приходит Partial<T>.
Синтаксис до безобразия прост: Partial<YourType>. Давайте посмотрим на примере.
// Исходный тип пользователя interface User { id: number; name: string; email: string; password: string; } // Допустим, мы хотим создать функцию для обновления пользователя. // Мы не можем требовать передать все поля, так как обновлять можно только одно из них. // Без Partial нам пришлось бы создавать новый тип вручную. interface UpdateUserRequest { id?: number; name?: string; email?: string; password?: string; } // Это работает, но нарушает принцип DRY. // Что если мы добавим новое поле в User? Нам придется не забыть добавить его и сюда. // Гораздо лучше и правильнее использовать Partial! function updateUser(id: number, fieldsToUpdate: Partial<User>) { // Здесь мы обращаемся к базе данных, находим пользователя с id // и обновляем только те поля, которые переданы в fieldsToUpdate. console.log(`Обновляем пользователя ${id}`, fieldsToUpdate); } // Теперь мы можем вызывать функцию, передавая любое подмножество свойств User! updateUser(1, { email: 'new-email@example.com' }); updateUser(2, { name: 'Иван', password: 'newSecretPassword' }); updateUser(3, {}); // Даже пустой объект является валидным аргументом!
Обратите внимание, как мы передаем в updateUser объект только с нужными полями. TypeScript не ругается, потому что Partial<User> ожидает объект, где каждое свойство является необязательным. Под капотом Partial реализован примерно так (это упрощенное представление):
type Partial<T> = { [P in keyof T]?: T[P]; };
Эта запись на языке маппирования типов означает: «Для каждого свойства P в наборе свойств типа T (keyof T), сделай это свойство необязательным (?) и сохрани его тип (T[P])».
Утилита Pick<T, K>
Следующая утилита в нашем списке, это Pick<T, K>. Ее название переводится как «выбрать» или «выдернуть». Ее задача создать новый тип, выбрав из исходного типа T только определенный набор свойств K.
Pick идеально подходит для ситуаций, когда вам нужна только небольшая часть свойств от большого типа. Например, у вас есть тип Product с десятком полей (id, название, описание, цена, вес, категория и т.д.). Для компонента карточки товара в интерфейсе вам могут понадобиться только название и цена. Вместо того чтобы описывать новый тип, вы можете «выдернуть» только необходимые поля с помощью Pick<T, K>.
Синтаксис: Pick<ИсходныйТип, 'поле1' | 'поле2' | 'поле3'>. Второй аргумент, это union тип из строковых литералов, перечисляющих имена нужных свойств.
Давайте продолжим с нашим типом User.
interface User { id: number; name: string; email: string; password: string; createdAt: Date; } // Допустим, мы хотим создать компонент, который отображает публичный профиль. // Этому компоненту нужны только id, name и email. Пароль и дата создания не нужны. // Создаем тип с помощью Pick. type UserPublicProfile = Pick<User, 'id' | 'name' | 'email'>; // Это эквивалентно ручному описанию: // type UserPublicProfile = { // id: number; // name: string; // email: string; // }; function renderUserProfile(user: UserPublicProfile) { console.log(`Имя: ${user.name}, Почта: ${user.email}`); } const user: UserPublicProfile = { id: 123, name: 'Максим', email: 'max@example.com' // password: 'secret' // Ошибка! Этого свойства здесь нет. }; renderUserProfile(user);
Как это работает под капотом? Примерная реализация Pick:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
Эта запись гласит: «Возьми множество свойств K (которое должно быть подмножеством keyof T) и для каждого свойства P в этом множестве K создай свойство в новом типе с тем же типом значения T[P]«.
Утилита Omit<T, K>
Третья героиня нашего урока, утилита Omit<T, K>. Ее название означает «опустить» или «пропустить». Она является полной противоположностью Pick. Ее задача создать новый тип, взяв все свойства исходного типа T и удалив из них указанный набор свойств K.
Omit невероятно полезна, когда вам нужно получить почти полную копию типа, но без нескольких конкретных полей. Классический пример, создание типа для данных, которые отправляются при регистрации пользователя. У вас есть тип User, но поле id обычно создается на стороне базы данных и передавать его при создании не нужно. Или, например, вы хотите исключить поле password перед отправкой данных о пользователе на клиент.
Синтаксис: Omit<ИсходныйТип, 'поле1' | 'поле2'>. Второй аргумент, это union тип из строковых литералов, перечисляющих имена свойств, которые нужно исключить.
Вернемся к нашему User.
interface User { id: number; name: string; email: string; password: string; createdAt: Date; } // Мы хотим создать тип для регистрации нового пользователя. // При создании пользователя мы не знаем его id и дату создания (их присвоит сервер), // и мы определенно не хотим передавать хэш пароля! // Давайте создадим тип, который исключает эти поля. type UserCreateInput = Omit<User, 'id' | 'createdAt'>; // Это эквивалентно ручному описанию: // type UserCreateInput = { // name: string; // email: string; // password: string; // }; function createUser(userData: UserCreateInput) { // ... логика создания пользователя console.log('Создаем пользователя:', userData.name); } createUser({ name: 'Анна', email: 'anna@example.com', password: 'superPassword123' // id: 999 // Ошибка! Этого свойства здесь быть не должно. });
Интересный факт: утилиту Omit можно выразить через Pick и Exclude (еще одну утилиту, которую мы рассмотрим позже). Ее реализация выглядит примерно так:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Это читается как: «Возьми все ключи типа T (keyof T), исключи из них ключи K (Exclude<keyof T, K>) и из результата создай новый тип с помощью Pick«.
Практические задачи и примеры
Давайте закрепим пройденный материал на более комплексных и жизненных примерах.
Задача 1: Форма обновления товара
У вас есть тип Product. Реализуйте функцию updateProduct, которая принимает id и объект для обновления. Этот объект может содержать любое подмножество свойств Product, кроме id (так как id используется для идентификации и не должен обновляться).
interface Product { id: number; name: string; price: number; description: string; inStock: boolean; } // Ваше решение: // 1. Нужно сделать все свойства Product необязательными. -> Partial<Product> // 2. Но нужно исключить свойство id, чтобы его нельзя было обновить. -> Omit<...> // Правильный подход: Omit<Partial<Product>, 'id'> function updateProduct(id: number, updatedFields: Omit<Partial<Product>, 'id'>) { // Логика обновления товара console.log(`Обновляем товар ${id} данными:`, updatedFields); } // Вызовы функции должны работать: updateProduct(101, { price: 1299 }); // OK updateProduct(102, { name: 'Новое название', inStock: false }); // OK updateProduct(103, {}); // OK // А этот вызов должен вызывать ошибку: updateProduct(104, { id: 999 }); // Ошибка! Нельзя обновлять id.
Задача 2: Безопасный пользователь для API
При отправке данных пользователя из API на фронтенд, необходимо исключить критичные поля, такие как password и hashedPassword. Создайте тип SafeUser, на основе User, который исключает эти поля.
interface User { id: number; login: string; password: string; hashedPassword: string; email: string; avatarUrl: string; } // Ваше решение: // Нужно исключить поля 'password' и 'hashedPassword'. -> Omit<...> type SafeUser = Omit<User, 'password' | 'hashedPassword'>; // Функция, которая имитирует получение пользователя из БД и возвращает "безопасную" версию. function getUserSafe(id: number): SafeUser { // ... логика получения пользователя из базы const userFromDb: User = { // ... все поля, включая password }; // Мы не можем вернуть userFromDb как есть, т.к. он содержит password. // Но мы можем создать объект типа SafeUser, выбрав нужные поля. // Для этого можно использовать тот же Omit в паре с Pick, но на уровне значений... // ...а проще и правильнее - "вручную" выбрать нужные поля или использовать библиотеки. const { password, hashedPassword, ...safeUser } = userFromDb; // Деструктуризация с rest! return safeUser; // Теперь safeUser содержит все, кроме password и hashedPassword. } const user = getUserSafe(1); console.log(user.login); // OK // console.log(user.password); // Ошибка! Свойства не существует.
Задача 3: Создание и обновление с общими полями
Часто бывает, что для создания (Create) и обновления (Update) сущности используются почти одинаковые типы, но с небольшими отличиями. Например, при создании id не нужен, а при обновлении обязателен. Покажите, как можно эффективно переиспользовать типы, используя Omit и Partial.
// Базовый тип со ВСЕМИ полями. interface Article { id: number; title: string; content: string; authorId: number; publishedAt: Date | null; } // Тип для создания статьи: id и publishedAt не задаются при создании. // Также authorId может быть присвоен автоматически из сессии, поэтому необязателен. type ArticleCreateInput = Omit<Partial<Article>, 'id' | 'publishedAt'> & { title: string; // Но title сделать обязательным обратно! authorId?: number; // authorId можно передать, а можно и нет. }; // Тип для обновления статьи: id обязателен, все остальное можно обновлять частично. // publishedAt и authorId менять probably нельзя, поэтому их исключаем. type ArticleUpdateInput = Pick<Article, 'id'> & Partial<Omit<Article, 'id' | 'publishedAt' | 'authorId'>>; // Функции для работы: function createArticle(article: ArticleCreateInput) { // ... логика } function updateArticle(article: ArticleUpdateInput) { // ... логика } createArticle({ title: 'Мой первый пост' }); // OK createArticle({ title: 'Пост', content: 'Текст...', authorId: 5 }); // OK // createArticle({ id: 1 }); // Ошибка: id нельзя передавать, а title обязателен. updateArticle({ id: 42, title: 'Обновленное название' }); // OK updateArticle({ id: 42 }); // OK, просто проверить наличие? // updateArticle({ title: 'Без ID' }); // Ошибка: нет id // updateArticle({ id: 42, authorId: 100 }); // Ошибка: authorId мы исключили из доступных для обновления
Этот пример сложнее, но он показывает реальную мощь комбинации утилит. Мы не просто механически исключаем поля, а составляем тип, который точно отражает бизнес-логику нашего приложения.
Другие полезные утилиты
Хотя Partial, Pick и Omit это «большая тройка», TypeScript предлагает и другие замечательные утилиты. Давайте бегло с ними познакомимся, чтобы вы знали об их существовании:
-
Required<T>. ПротивоположностьPartial. Делает все свойства в типеTобязательными. -
Readonly<T>. Создает тип, в котором все свойства только для чтения. Попытка их изменить приведет к ошибке компиляции. -
Record<K, T>. Создает тип объекта, у которого ключи имеют типK, а значения типT. Идеально подходит для создания словарей.Record<string, number>это объект с произвольными строковыми ключами и числовыми значениями. -
Exclude<T, U>. Исключает из типаTвсе типы, которые можно присвоить типуU. Чаще используется с union-типами.Exclude<'a' | 'b' | 'c', 'a' | 'b'>даст результат'c'. -
Extract<T, U>. ПротивоположностьExclude. Извлекает из типаTвсе типы, которые можно присвоить типуU. -
NonNullable<T>. Удаляетnullиundefinedиз типаT.
Изучение этих утилит значительно повысит ваш уровень владения TypeScript.
Заключение
Поздравляю! Вы только что освоили один из самых мощных инструментов в арсенале TypeScript-разработчика. Утилиты типов Partial, Pick и Omit, это ваш скальпель для точной и аккуратной работы с типами. Они помогут вам избежать дублирования кода, сделать ваши типы более выразительными и тесно связанными с предметной областью.
Главный принцип, который стоит запомнить:
-
Используйте
Partial<T>, когда нужно сделать свойства необязательными. -
Используйте
Pick<T, K>, когда нужно выбрать конкретные свойства. -
Используйте
Omit<T, K>, когда нужно исключить конкретные свойства.
Не бойтесь их комбинировать, как мы это делали в практических задачах (Omit<Partial<T>, 'id'>). Именно в комбинациях раскрывается их истинная сила. Скоро вы будете удивляться, как же вы жили без них.
Это был урок №38 из моего полного курса по TypeScript для начинающих. Если вы только присоединились к нам или хотите повторить предыдущие материалы, вам сюда: Полный курс по TypeScript для начинающих
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


