Зачем вообще нужны эти "утилитные типы"?
Если вы только начинаете изучать TypeScript, вы наверняка столкнулись с ситуацией: у вас есть тип (описание структуры данных), и вам нужен почти такой же, но чуть-чуть другой. Например, у вас есть тип "Пользователь" с 10 полями, а для формы редактирования нужны только 3 из них.
Можно, конечно, написать новый тип руками. Но если у вас 50 типов и каждый нужен в 3-4 вариациях - вы утонете в дублирующемся коде. Вот тут на помощь приходят утилитные типы - специальные "трансформаторы", которые берут существующий тип и делают из него новый.
Давайте разберём их все на простых примерах. Обещаю - к концу статьи вы будете использовать их как профи (ну, или хотя бы перестанете пугаться, увидев Partial<Omit<User, 'id'>> в чужом коде).
Наш пример для всей статьи
Чтобы не придумывать новые примеры каждый раз, возьмём один тип и будем работать с ним:
// Описание пользователя на нашем сайте
// interface - это как "чертёж", описание структуры данных
interface User {
id: number; // Уникальный номер пользователя (1, 2, 3...)
name: string; // Имя ("Иван")
email: string; // Электронная почта ("ivan@example.com")
avatar: string; // Ссылка на аватарку ("/photos/ivan.jpg")
role: 'admin' | 'user'; // Роль: или админ, или обычный пользователь
createdAt: Date; // Дата регистрации
}
Partial - делает все поля необязательными
Представьте: пользователь хочет изменить только своё имя. Ему не нужно отправлять email, аватарку и всё остальное - только имя. Но наш тип User требует ВСЕ поля. Что делать?
Partial берёт тип и делает все поля в нём необязательными (добавляет знак ? к каждому полю):
// Partial делает все поля необязательными
// Было: { id: number, name: string, email: string, ... }
// Стало: { id?: number, name?: string, email?: string, ... }
// Знак ? означает "это поле можно не указывать"
type UpdateUserData = Partial<User>;
// Теперь можно передать только те поля, которые меняются
async function updateUser(userId: number, data: Partial<User>) {
// data может содержать любую комбинацию полей User
// или не содержать ничего (пустой объект)
await db.users.update(userId, data);
}
// Примеры использования:
updateUser(1, { name: 'Новое имя' }); // Меняем только имя
updateUser(1, { email: 'new@mail.com' }); // Только email
updateUser(1, { name: 'Иван', avatar: '/new.jpg' }); // Имя и аватар
// Всё это валидно! Partial разрешает любую комбинацию
Required - делает все поля обязательными
Противоположность Partial. Если у вас тип, где некоторые поля необязательные, Required сделает их все обязательными:
// Настройки приложения - некоторые поля можно не указывать
interface AppSettings {
theme?: string; // ? значит необязательно
language?: string;
fontSize?: number;
notifications?: boolean;
}
// Required убирает все знаки ? - теперь ВСЕ поля обязательны
type FullSettings = Required<AppSettings>;
// { theme: string, language: string, fontSize: number, notifications: boolean }
// Полезно для функции, которая гарантирует заполнение всех настроек
function getFullSettings(partial: AppSettings): FullSettings {
// Если пользователь не указал настройку - ставим значение по умолчанию
return {
theme: partial.theme ?? 'dark', // ?? означает "если не указано, то..."
language: partial.language ?? 'ru',
fontSize: partial.fontSize ?? 14,
notifications: partial.notifications ?? true
};
}
Pick - выбираем только нужные поля
Допустим, для карточки пользователя на странице нужны только имя и аватар. Зачем тащить весь тип User с 6 полями? Pick позволяет "выбрать" только нужные поля:
// Pick<ОткудаБерём, КакиеПоляНужны>
// Для карточки пользователя нужны только имя и аватар
type UserCard = Pick<User, 'name' | 'avatar'>;
// Результат: { name: string, avatar: string }
// Остальные поля (id, email, role, createdAt) - отброшены
// Для выпадающего списка - только id и имя
type UserOption = Pick<User, 'id' | 'name'>;
// Результат: { id: number, name: string }
// Практический пример: API возвращает только нужные поля
app.get('/api/users/cards', async (req, res) => {
// Запрашиваем из базы только 2 поля вместо всех 6
// Это быстрее и экономит трафик
const users: UserCard[] = await db.query(
'SELECT name, avatar FROM users'
);
res.json(users);
});
Omit - убираем ненужные поля
Omit - это противоположность Pick. Вместо "возьми эти поля" говорим "убери эти поля, остальное оставь":
// Omit<ОткудаБерём, КакиеПоляУбрать>
// Для создания пользователя не нужны id и createdAt
// (id генерирует база данных, createdAt ставится автоматически)
type CreateUser = Omit<User, 'id' | 'createdAt'>;
// Результат: { name: string, email: string, avatar: string, role: 'admin' | 'user' }
// Для отправки данных клиенту убираем секретные поля
type PublicUser = Omit<User, 'role'>;
// Результат: { id: number, name: string, email: string, avatar: string, createdAt: Date }
// Практический пример: функция регистрации
async function register(data: CreateUser) {
// data содержит name, email, avatar, role
// Но НЕ содержит id и createdAt - их создаст база данных
const [result] = await db.execute(
'INSERT INTO users (name, email, avatar, role) VALUES (?, ?, ?, ?)',
[data.name, data.email, data.avatar, data.role]
);
return result;
}
Record - создаём словарь
Record создаёт тип "словаря" - объект, где ключи и значения имеют определённый тип. Это как настоящий словарь: слово (ключ) и его перевод (значение):
// Record<ТипКлюча, ТипЗначения>
// Словарь переводов: ключ - код языка, значение - текст
type Translations = Record<'ru' | 'en', string>;
const greeting: Translations = {
ru: 'Привет!',
en: 'Hello!'
};
// greeting.ru = "Привет!"
// greeting.en = "Hello!"
// greeting.de = ... // Ошибка! "de" не входит в 'ru' | 'en'
// Маппинг статусов заказа на цвета (для отображения на сайте)
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered';
const statusColors: Record<OrderStatus, string> = {
pending: '#ffa500', // Оранжевый - ожидает
processing: '#3498db', // Синий - обрабатывается
shipped: '#9b59b6', // Фиолетовый - отправлен
delivered: '#00ff88' // Зелёный - доставлен
};
// Теперь если добавить новый статус, TypeScript напомнит
// что нужно добавить для него цвет. Удобно!
Readonly - запрет на изменение
Иногда нужно убедиться, что данные не будут случайно изменены. Readonly делает все поля доступными только для чтения:
// Readonly запрещает изменение полей после создания
// Конфигурация приложения - не должна меняться после загрузки
type AppConfig = Readonly<{
apiUrl: string;
version: string;
maxRetries: number;
}>;
const config: AppConfig = {
apiUrl: 'https://api.example.com',
version: '2.0.0',
maxRetries: 3
};
// config.apiUrl = 'другой url'; // ОШИБКА!
// TypeScript скажет: "Нельзя менять свойство apiUrl, оно readonly"
// Это защищает от случайных изменений в коде
Комбинируем типы (вот тут начинается магия)
Настоящая сила утилитных типов раскрывается, когда вы их комбинируете:
// Пример 1: Для обновления профиля
// - убираем id и createdAt (их нельзя менять)
// - делаем остальные поля необязательными
type UpdateProfile = Partial<Omit<User, 'id' | 'createdAt'>>;
// Результат: { name?: string, email?: string, avatar?: string, role?: 'admin' | 'user' }
// Пример 2: Для отображения списка пользователей
// - берём только id, name, avatar
// - делаем readonly (только для показа)
type UserListItem = Readonly<Pick<User, 'id' | 'name' | 'avatar'>>;
// Результат: { readonly id: number, readonly name: string, readonly avatar: string }
// Пример 3: Ответ API со списком
// Можно создать переиспользуемый тип для любых списков
type PaginatedList<T> = {
items: T[]; // Массив элементов
total: number; // Всего элементов
page: number; // Текущая страница
hasMore: boolean; // Есть ли ещё страницы
};
// Используем для разных сущностей
type UserListResponse = PaginatedList<UserListItem>;
type ProductListResponse = PaginatedList<Pick<Product, 'id' | 'name' | 'price'>>;
// В API:
app.get('/api/users', async (req, res) => {
const response: UserListResponse = {
items: users,
total: 150,
page: 1,
hasMore: true
};
res.json(response);
});
Создаём свои утилитные типы
Когда встроенных типов не хватает, можно создать свои:
// PartialBy - делает необязательными только УКАЗАННЫЕ поля
// (в отличие от Partial, который делает ВСЕ необязательными)
type PartialBy<T, K extends keyof T> =
Omit<T, K> & Partial<Pick<T, K>>;
// Для создания пользователя: id необязателен, остальное обязательно
type CreateUser = PartialBy<User, 'id' | 'createdAt'>;
// { name: string, email: string, avatar: string, role: ..., id?: number, createdAt?: Date }
// Nullable - разрешает null для указанных полей
type Nullable<T, K extends keyof T> = {
[P in keyof T]: P extends K ? T[P] | null : T[P];
};
// Пользователь может не иметь аватарки
type UserWithOptionalAvatar = Nullable<User, 'avatar'>;
// avatar теперь может быть string ИЛИ null
Реальный пример: типизация API
// Описываем все возможные ответы API
type ApiResponse<T> =
| { success: true; data: T } // Успех: есть данные
| { success: false; error: string }; // Ошибка: есть сообщение
// Функция для запроса к API
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
try {
const res = await fetch(url);
const data = await res.json();
return { success: true, data };
} catch (err) {
return { success: false, error: 'Что-то пошло не так' };
}
}
// Использование
const response = await fetchApi<User[]>('/api/users');
if (response.success) {
// TypeScript знает: здесь response.data - это User[]
console.log(response.data[0].name);
} else {
// TypeScript знает: здесь response.error - это string
console.error(response.error);
}
// Компилятор не даст обратиться к data в ветке ошибки
// и к error в ветке успеха. Класс!
Итого
Утилитные типы - это не магия, а просто удобные инструменты для трансформации типов. Вот шпаргалка:
Partial<T>- все поля необязательные (для форм обновления)Required<T>- все поля обязательные (для валидации конфигов)Pick<T, K>- только выбранные поля (для карточек, списков)Omit<T, K>- все поля кроме указанных (для создания записей)Record<K, V>- словарь с заданными ключами и значениямиReadonly<T>- запрет на изменение (для конфигов, констант)
Начните с Pick и Omit - они нужны буквально в каждом проекте. А когда освоитесь, попробуйте комбинации типа Partial<Omit<User, 'id'>> - и почувствуете себя волшебником. Ну, или хотя бы человеком, который не копирует интерфейсы ради изменения одного поля.