Utility Types
Встроенные generic-типы TypeScript для трансформации существующих типов: Partial, Required, Readonly, Pick, Omit, Record и другие.
Зачем нужно
- Не писать дублирующиеся типы вручную
- Трансформировать существующие типы: сделать все поля опциональными, выбрать подмножество, сделать readonly
- Стандартные утилиты понятны всем TypeScript-разработчикам
- Уменьшение boilerplate при работе с API, формами, состоянием
Где используется
Partial<T>— формы, обновления (PATCH-запросы)Pick<T, K>/Omit<T, K>— DTO, подмножество полейRecord<K, V>— словари, маппингиReturnType<T>— извлечение типа из функцииAwaited<T>— распаковка Promise
Предпосылки
- Generics — параметрические типы
- Интерфейсы — описание объектов
- typeof и keyof — keyof для работы с ключами
Partial<T> — все свойства опциональные
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Все поля становятся опциональными
type PartialUser = Partial<User>;
// {
// id?: number;
// name?: string;
// email?: string;
// age?: number;
// }
// Применение: обновление (PATCH)
function updateUser(id: number, updates: Partial<User>): User {
const user = getUser(id);
return { ...user, ...updates };
}
updateUser(1, { name: "Bob" }); // OK
updateUser(1, { age: 25, email: "b@b" }); // OK
updateUser(1, {}); // OK
// Реализация:
// type Partial<T> = { [K in keyof T]?: T[K] };
Required<T> — все свойства обязательные
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
// Все поля обязательные
type FullConfig = Required<Config>;
// {
// host: string;
// port: number;
// debug: boolean;
// }
// Применение: валидированная конфигурация
function startServer(config: Required<Config>): void {
console.log(`Starting on ${config.host}:${config.port}`);
}
// Реализация:
// type Required<T> = { [K in keyof T]-?: T[K] };
Readonly<T> — все свойства только для чтения
interface User {
id: number;
name: string;
}
type ReadonlyUser = Readonly<User>;
// {
// readonly id: number;
// readonly name: string;
// }
const user: ReadonlyUser = { id: 1, name: "Alice" };
user.name = "Bob"; // Ошибка! Cannot assign to 'name'
// Применение: иммутабельное состояние
function freeze<T>(obj: T): Readonly<T> {
return Object.freeze(obj);
}
// Реализация:
// type Readonly<T> = { readonly [K in keyof T]: T[K] };
Pick<T, K> — выбрать свойства
interface User {
id: number;
name: string;
email: string;
age: number;
role: string;
}
// Выбираем только нужные свойства
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }
type UserContact = Pick<User, "name" | "email">;
// { name: string; email: string }
// Применение: API response DTO
function getUserPreview(id: number): Pick<User, "id" | "name" | "role"> {
const user = getUser(id);
return { id: user.id, name: user.name, role: user.role };
}
// Реализация:
// type Pick<T, K extends keyof T> = { [P in K]: T[P] };
Omit<T, K> — исключить свойства
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Убираем чувствительные поля
type SafeUser = Omit<User, "password">;
// { id: number; name: string; email: string; createdAt: Date }
// Данные для создания (без автогенерируемых полей)
type CreateUserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; password: string }
// Применение: API DTO
function createUser(input: Omit<User, "id" | "createdAt">): User {
return {
...input,
id: generateId,
createdAt: new Date,
};
}
// Реализация:
// type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Record<K, V> — объект с ключами K и значениями V
// Словарь
type UserMap = Record<string, User>;
const users: UserMap = {
alice: { id: 1, name: "Alice", email: "a@b.com" },
bob: { id: 2, name: "Bob", email: "b@b.com" },
};
// Enum-ключи
type Role = "admin" | "user" | "guest";
type RolePermissions = Record<Role, string>;
const permissions: RolePermissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};
// Маппинг статусов
type StatusColor = Record<"success" | "error" | "warning", string>;
const colors: StatusColor = {
success: "#00ff00",
error: "#ff0000",
warning: "#ffff00",
};
// Реализация:
// type Record<K extends keyof any, T> = { [P in K]: T };
Extract<T, U> — извлечь типы из union
type AllTypes = string | number | boolean | null | undefined;
// Извлечь только те, что extends string | number
type Primitives = Extract<AllTypes, string | number>;
// string | number
// Из событий
type Event = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type MouseEvent = Extract<Event, "click" | "scroll" | "mousemove">;
// "click" | "scroll" | "mousemove"
// Реализация:
// type Extract<T, U> = T extends U ? T : never;
Exclude<T, U> — исключить типы из union
type AllTypes = string | number | boolean | null | undefined;
// Убрать null и undefined
type NonNullTypes = Exclude<AllTypes, null | undefined>;
// string | number | boolean
// Из событий
type Event = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type NonMouseEvent = Exclude<Event, "click" | "scroll" | "mousemove">;
// "keydown" | "keyup"
// Реализация:
// type Exclude<T, U> = T extends U ? never : T;
NonNullable<T> — убрать null и undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
// Применение
function process(value: string | null | undefined): string {
const safe: NonNullable<typeof value> = value!; // assertion
return safe.toUpperCase();
}
// Реализация:
// type NonNullable<T> = T & {};
// (эквивалентно Exclude<T, null | undefined>)
ReturnType<T> — тип возвращаемого значения функции
function getUser() {
return { id: 1, name: "Alice", email: "alice@example.com" };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string }
// Для async функций
async function fetchUsers() {
return [{ id: 1, name: "Alice" }];
}
type FetchResult = ReturnType<typeof fetchUsers>;
// Promise<{ id: number; name: string }>
// Для получения распакованного типа
type Users = Awaited<ReturnType<typeof fetchUsers>>;
// { id: number; name: string }
// Реализация:
// type ReturnType<T extends (...args: any) => any> =
// T extends (...args: any) => infer R ? R : any;
Parameters<T> — типы параметров функции
function createUser(name: string, age: number, isAdmin: boolean): void {}
type Params = Parameters<typeof createUser>;
// [string, number, boolean]
// Извлечение конкретного параметра
type FirstParam = Parameters<typeof createUser>[0]; // string
type SecondParam = Parameters<typeof createUser>[1]; // number
// Применение: обёртка функции
function withLogging<T extends (...args: any) => any>(
fn: T
): (...args: Parameters<T>) => ReturnType<T> {
return (...args) => {
console.log("Calling with:", args);
return fn(...args);
};
}
// Реализация:
// type Parameters<T extends (...args: any) => any> =
// T extends (...args: infer P) => any ? P : never;
Awaited<T> — распаковка Promise
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number (рекурсивно)
type C = Awaited<string | Promise<number>>; // string | number
// Применение с async функциями
async function fetchData: Promise<{ users: User }> {
// ...
}
type Data = Awaited<ReturnType<typeof fetchData>>;
// { users: User }
Комбинирование utility types
interface User {
id: number;
name: string;
email: string;
password: string;
role: "admin" | "user";
createdAt: Date;
updatedAt: Date;
}
// CREATE: без id и timestamps
type CreateUser = Omit<User, "id" | "createdAt" | "updatedAt">;
// UPDATE: все поля кроме id опциональные
type UpdateUser = Partial<Omit<User, "id">> & Pick<User, "id">;
// PUBLIC: без пароля, readonly
type PublicUser = Readonly<Omit<User, "password">>;
// FILTER: только для поиска
type UserFilter = Partial<Pick<User, "name" | "email" | "role">>;
// LIST RESPONSE
type UserListResponse = {
data: PublicUser;
total: number;
page: number;
};
Частые ошибки
Omitс несуществующим ключом — не даст ошибку, просто ничего не исключит
type Result = Omit<User, "nonExistent">; // Нет ошибки! Возвращает весь User
Partialделает неглубокий partial — вложенные объекты остаются обязательными
interface Config {
server: { host: string; port: number }; // Всё ещё Required
}
type P = Partial<Config>;
// { server?: { host: string; port: number } }
// server опционален, но если задан — host и port обязательны
Readonly— поверхностный — вложенные объекты можно мутировать- Забыть
typeofпри ReturnType — нужен тип функции, а не сама функция
function getUser() { return { name: "Alice" }; }
type A = ReturnType<getUser>; // Ошибка!
type B = ReturnType<typeof getUser>; // OK
Практика
- Создайте CRUD-типы из одного интерфейса:
CreateDTO,UpdateDTO,ResponseDTO - Реализуйте
DeepPartial<T>вручную (рекурсивный Partial) - Используйте
Recordдля создания словаря с enum-ключами - Извлеките тип возвращаемого значения async-функции через
Awaited<ReturnType<>> - Комбинируйте
Pick+Partial+Requiredдля формы с обязательными и опциональными полями
Связанные темы
- Generics — основа utility types
- Mapped types — как устроены utility types внутри
- Conditional types — Extract, Exclude, ReturnType внутри
- typeof и keyof — keyof для Pick, Omit
Ресурсы
- Utility Types
- Type Challenges — реализация utility types с нуля
- Total TypeScript — Utility Types