← Назад к вопросам

Для чего нужны Utility Types?

1.6 Junior🔥 91 комментариев
#TypeScript

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Для чего нужны Utility Types?

Utility Types в TypeScript — это встроенные обобщённые типы, которые позволяют преобразовывать и модифицировать существующие типы, не переписывая их с нуля. Это мощный инструмент для создания типов на основе других типов, снижая дублирование кода и повышая переиспользуемость.

Основная идея

Вместо того чтобы создавать похожие типы вручную, используй Utility Types для трансформации:

// ❌ Плохо: много дублирования
interface User {
  id: number;
  name: string;
  email: string;
}

interface PartialUser {
  id?: number;
  name?: string;
  email?: string;
}

interface ReadonlyUser {
  readonly id: number;
  readonly name: string;
  readonly email: string;
}

// ✅ Хорошо: используем Utility Types
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;

Популярные Utility Types

1. Partial<T> — все поля опциональны

interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = Partial<User>;
// Эквивалент:
// {
//   id?: number;
//   name?: string;
//   email?: string;
// }

const updateUser: PartialUser = {
  name: 'John' // только name, остальное опционально
};

2. Required<T> — все поля обязательны

type PartialUser = {
  id?: number;
  name?: string;
  email?: string;
};

type RequiredUser = Required<PartialUser>;
// Эквивалент:
// {
//   id: number;
//   name: string;
//   email: string;
// }

const user: RequiredUser = {
  id: 1,
  name: 'John',
  email: 'john@example.com' // все поля обязательны
};

3. Readonly<T> — все поля неизменяемы

interface User {
  id: number;
  name: string;
}

type ReadonlyUser = Readonly<User>;

const user: ReadonlyUser = { id: 1, name: 'John' };
user.name = 'Jane'; // ❌ Ошибка: Cannot assign to 'name' because it is a read-only property

4. Pick<T, K> — выбрать определённые поля

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

type UserPreview = Pick<User, 'id' | 'name'>;
// Эквивалент: { id: number; name: string }

const preview: UserPreview = {
  id: 1,
  name: 'John'
  // email и age НЕ требуются
};

5. Omit<T, K> — исключить определённые поля

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

type PublicUser = Omit<User, 'password'>;
// Эквивалент: { id: number; name: string; email: string }

const publicUser: PublicUser = {
  id: 1,
  name: 'John',
  email: 'john@example.com'
  // password исключена
};

6. Record<K, T> — создать объект с определёнными ключами

type Status = 'active' | 'inactive' | 'pending';

type StatusConfig = Record<Status, { label: string; color: string }>;

const config: StatusConfig = {
  active: { label: 'Active', color: 'green' },
  inactive: { label: 'Inactive', color: 'gray' },
  pending: { label: 'Pending', color: 'yellow' }
  // TypeScript проверит, что все статусы есть!
};

7. Extract<T, U> — извлечь пересекающиеся типы

type Status = 'active' | 'inactive' | 'pending' | 'deleted';

type ActionableStatus = Extract<Status, 'active' | 'inactive'>;
// Эквивалент: 'active' | 'inactive'

const status: ActionableStatus = 'active'; // ✅ OK
const status2: ActionableStatus = 'deleted'; // ❌ Ошибка

8. Exclude<T, U> — исключить типы

type Status = 'active' | 'inactive' | 'pending' | 'deleted';

type VisibleStatus = Exclude<Status, 'deleted'>;
// Эквивалент: 'active' | 'inactive' | 'pending'

const status: VisibleStatus = 'deleted'; // ❌ Ошибка

9. Keyof<T> — получить ключи типа

interface User {
  id: number;
  name: string;
  email: string;
}

type UserKeys = keyof User;
// Эквивалент: 'id' | 'name' | 'email'

const key: UserKeys = 'name'; // ✅ OK
const key2: UserKeys = 'age'; // ❌ Ошибка

10. ReturnType<T> — получить тип возврата функции

function getUser(): { id: number; name: string } {
  return { id: 1, name: 'John' };
}

type UserType = ReturnType<typeof getUser>;
// Эквивалент: { id: number; name: string }

Практические примеры

Пример 1: API Request и Response

interface CreateUserRequest {
  name: string;
  email: string;
  password: string;
}

// PublicUser скрывает password
type UserResponse = Omit<CreateUserRequest, 'password'>;

const handleCreateUser = async (
  data: CreateUserRequest
): Promise<UserResponse> => {
  // ... сохраняем пользователя
  const { password, ...user } = data;
  return user; // password не отправляется
};

Пример 2: Form Validation

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Для формы редактирования требуются только некоторые поля
type UserFormData = Partial<Pick<User, 'name' | 'email'>>;

const handleFormSubmit = (data: UserFormData) => {
  // data может содержать только name, только email, или оба
};

Пример 3: Configuration Object

interface AppConfig {
  apiUrl: string;
  apiKey: string;
  timeout: number;
  retries: number;
}

// Некоторые параметры опциональны при создании
type PartialConfig = Partial<AppConfig>;
type DefaultConfig = Required<AppConfig>; // все обязательны

const createApp = (config: PartialConfig): AppConfig => {
  return {
    apiUrl: config.apiUrl || 'https://api.example.com',
    apiKey: config.apiKey || process.env.API_KEY!,
    timeout: config.timeout || 5000,
    retries: config.retries || 3
  };
};

Пример 4: Event Handler Typing

type Events = {
  'user-login': { userId: number };
  'user-logout': { timestamp: number };
  'error': { message: string; code: number };
};

type EventHandlers = Record<keyof Events, (data: Events[keyof Events]) => void>;

// TypeScript проверит, что все события обработаны
const handlers: EventHandlers = {
  'user-login': (data) => console.log(data.userId),
  'user-logout': (data) => console.log(data.timestamp),
  'error': (data) => console.error(data.message, data.code)
};

Создание собственных Utility Types

// Кастомный Utility Type: получить все необязательные ключи
type Optional<T> = {
  [K in keyof T]?: T[K];
};

// Кастомный: сделать все поля строками
type Stringify<T> = {
  [K in keyof T]: string;
};

// Кастомный: сделать свойства функциями
type Callable<T> = {
  [K in keyof T]: () => T[K];
};

interface User {
  id: number;
  name: string;
}

type CallableUser = Callable<User>;
// {
//   id: () => number;
//   name: () => string;
// }

Когда использовать Utility Types?

✅ Используй, если:

  • Нужно избежать дублирования типов
  • Трансформируешь существующие типы
  • Работаешь с API ответами
  • Создаёшь generics

❌ Не используй, если:

  • Проще написать тип с нуля
  • Трансформация очень специфична
  • Усложняет читаемость кода

Лучшие практики

  1. Комбинируй Utility Types для сложных трансформаций:
type UpdateUserDTO = Partial<Omit<User, 'id' | 'createdAt'>>;
  1. Документируй кастомные Utility Types:
/** Убирает поле password из User для публичного использования */
type PublicUser = Omit<User, 'password'>;
  1. Используй Pick для API контрактов:
type UserListItem = Pick<User, 'id' | 'name' | 'avatar'>;

Utility Types — это мощный инструмент для написания DRY, типобезопасного и поддерживаемого TypeScript кода.

Для чего нужны Utility Types? | PrepBro