Utility-типы TypeScript, которые экономят часы: Record, Omit, ReturnType
Большинство разработчиков на TypeScript знают interface и type, но не используют встроенные utility-типы — а они убирают горы дублирования и багов синхронизации. Когда вы копируете интерфейс и потом забываете обновить копию при изменении оригинала — это классический источник ошибок. Utility-типы позволяют вывести один тип из другого, и изменение оригинала автоматически подтягивается всюду.
Базовый набор
Partial<T> — все поля опциональны
Делает все поля типа необязательными. Идеально для форм редактирования, PATCH-запросов, конфигурации с дефолтами:
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
type UserPatch = Partial<User>;
// Эквивалент: { id?: string; name?: string; email?: string; role?: 'admin' | 'user' }
function updateUser(id: string, patch: UserPatch) {
// Можно передать только изменившиеся поля
}
Pick<T, Keys> и Omit<T, Keys> — выделить или убрать
Вместо ручного копирования интерфейса (которое рассинхронизируется при изменении User) — выведите часть типа:
// Только публичные данные для отображения
type PublicUser = Pick<User, 'id' | 'name'>;
// { id: string; name: string }
// Всё, кроме чувствительных полей
type SafeUser = Omit<User, 'email' | 'role'>;
// { id: string; name: string }
Измените User — PublicUser и SafeUser обновятся автоматически. Никаких забытых копий.
Record<K, V> — типизированный словарь
Замена кривым конструкциям вроде { [key: string]: User } или, что хуже, any:
// Словарь «id → пользователь»
const usersById: Record<string, User> = {};
// Статус по этапу процесса (ключи ограничены)
type Stage = 'draft' | 'review' | 'published';
const stageLabel: Record<Stage, string> = {
draft: 'Черновик',
review: 'На ревью',
published: 'Опубликован',
};
// Забыли один ключ — ошибка компиляции. Лишний ключ — тоже ошибка.
Readonly<T> — иммутабельная версия
Все поля помечаются readonly, изменять нельзя. Полезно для констант и для защиты от случайной мутации пропсов:
const config: Readonly<ApiConfig> = { baseUrl: '/api', timeout: 5000 };
config.timeout = 3000; // Ошибка компиляции
ReturnType<F> и Parameters<F> — вывести из функции
Если функция уже описана, можно вытащить тип её результата или параметров без дублирования:
function fetchUser(id: string): Promise<User> { /* ... */ }
type UserPromise = ReturnType<typeof fetchUser>; // Promise<User>
type UserId = Parameters<typeof fetchUser>[0]; // string
Особенно полезно, когда API-клиент типизирован автоматически (например, сгенерирован из OpenAPI-схемы) — типы ответов можно вытаскивать, не описывая их вручную.
Как они устроены: mapped types
Все встроенные utility-типы собраны из двух механизмов — mapped types (преобразование каждого поля) и conditional types (логика на типах).
Partial буквально это:
type Partial<T> = {
[K in keyof T]?: T[K];
};
Читается: «для каждого ключа K из ключей T — поле с тем же типом T[K], но опциональное (?)». Точно так же Readonly — это [K in keyof T]: readonly T[K]. Pick:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Поняв mapped types, вы можете писать свои utility-типы под конкретные задачи:
// Сделать все методы объекта асинхронными
type Asyncify<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[K];
};
Условные типы и infer
extends в типах работает как if:
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'
infer — вывести тип «по дороге». Часто используется в комбинации:
// Вытащить тип элементов массива
type ElementOf<T> = T extends (infer E)[] ? E : never;
type X = ElementOf<string[]>; // string
type Y = ElementOf<User[]>; // User
Практические кейсы
Типизация API-клиента по ответам. Если бэкенд отдаёт данные, а у вас есть тип ответа серверной функции (или сгенерированный из схемы) — выведите типы на клиенте из него, не дублируя интерфейсы:
type CreateUserResponse = Awaited<ReturnType<typeof api.users.create>>;
// Один источник истины — функция api. Всё остальное выводится.
React-пропсы, выведенные из компонентов. Если у компонента есть пропсы, иногда удобнее вытащить их из самого компонента, чем описывать отдельно:
type ButtonProps = ComponentProps<typeof Button>;
Формы редактирования. Partial<T> для незаполненных полей, Pick<T, Keys> для формы, где редактируется подмножество полей. Без ручного переписывания.
Когда utility-типы вредны
Навешивая их слоями, можно получить тип, который невозможно прочитать:
// Стало нечитаемо — проще выписать обычный interface
type Mess = Partial<Record<keyof Omit<User, 'id'>, ReadonlyArray<ReturnType<typeof f>>>>;
Правило: если тип занимает строку и его нельзя понять с первого взгляда — вынесите промежуточные шаги в type-алиасы или, возможно, проще описать обычный interface. Utility-типы — инструмент, а не цель.
Короткое summary
Utility-типы TypeScript (Partial, Pick, Omit, Record, Readonly, ReturnType, Parameters) — это способ выводить типы из других типов вместо ручного копирования, и тем самым убирать баги рассинхронизации. Под капотом они собраны из mapped types и условных типов с infer, которые позволяют описывать собственные преобразования. Главный практический эффект — типизация API-клиента, форм редактирования и React-пропсов по одному источнику истины. Мера: не наслаивать их в нечитаемое месиво — иногда обычный interface понятнее.
Что почитать
- TypeScript Handbook: Utility Types — полный список встроенных.
- TypeScript Handbook: Mapped Types — как устроены
Partial/Pick/Readonlyпод капотом. - Type体操: Type Challenges — практика по условным и mapped типам.