$ sudo teach IT
МОДУЛЬ 6 · УРОК 5

Утилитарные типы: Partial, Required, Pick, Omit

Встроенные типы для трансформации типов данных

Цели урока

  • Разобрать утилитарный тип Partial<T>
  • Разобрать утилитарный тип Required<T>
  • Разобрать утилитарный тип Pick<T, K>
  • Разобрать утилитарный тип Omit<T, K>
  • Научиться комбинировать утилитарные типы

Что такое утилитарные типы?

TypeScript поставляется с набором встроенных утилитарных типов, которые позволяют трансформировать существующие типы. Они работают как функции для типов: принимают один тип и возвращают новый.

Аналогия:

Утилитарные типы — как функции-трансформеры для данных, только на уровне типов. Они берут «сырой» тип и создают на его основе новый, изменённый тип.

Все утилитарные типы определены в стандартной библиотеке TypeScript. Вы можете использовать их прямо сейчас:

// Все эти типы определены в lib.es5.d.ts
// Но вы можете использовать их без дополнительных импортов:

type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

Partial<T> — все свойства необязательные

Partial<T> создаёт тип, где все свойства оригинального типа становятся необязательными:

// Оригинальный интерфейс
interface User {
  name: string;
  age: number;
  email: string;
}

// Partial делает все свойства необязательными
type PartialUser = Partial<User>;
// Эквивалентно:
// type PartialUser = {
// name?: string;
// age?: number;
// email?: string;
// };

// Теперь можно создать объект с любым подмножеством свойств:
const partialUser1: PartialUser = { name: "Алиса" }; // OK!
const partialUser2: PartialUser = { age: 25 }; // OK!
const partialUser3: PartialUser = {}; // OK!

Partial удобен для:

  • Функций обновления (update): передаём только изменённые поля
  • Функций поиска (filter): указываем только нужные критерии
  • Дефолтных значений: задаём только часть полей
// Практический пример: функция обновления
function updateUser(
  id: number,
  updates: Partial<User>
): void {
  // Обновляем только переданные поля
  console.log(`Обновление пользователя ${id}`, updates);
}

// Можно обновить любое подмножество полей:
updateUser(1, { name: "Новое имя" }); // OK!
updateUser(1, { age: 30, email: "new@email.com" }); // OK!
updateUser(1, {}); // OK! Ничего не меняем

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

Required<T> — противоположность Partial. Он делает все свойства обязательными, убирая модификатор ?:

// Интерфейс с необязательными свойствами
interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

// Required делает все свойства обязательными
type FullConfig = Required<Config>;
// Эквивалентно:
// type FullConfig = {
// host: string;
// port: number;
// debug: boolean;
// };

// Теперь все свойства обязательны:
const config1: FullConfig = {
  host: "localhost",
  port: 3000,
  debug: true
}; // OK!

// Ошибка: отсутствует debug:
// const config2: FullConfig = { host: "localhost", port: 3000 }; // Ошибка!

Required удобен, когда:

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

Pick<T, K> — выбор свойств

Pick<T, K> создаёт тип, содержащий только выбранные свойства из оригинального типа:

// Оригинальный интерфейс
interface User {
  name: string;
  age: number;
  email: string;
  password: string;
}

// Pick выбирает только name и email
type UserPublic = Pick<User, "name" | "email">;
// Эквивалентно:
// type UserPublic = {
// name: string;
// email: string;
// };

// Использование:
const publicUser: UserPublic = {
  name: "Алиса",
  email: "a@b.com"
}; // OK!

// Ошибка: password не входит в тип UserPublic:
// const user2: UserPublic = {
// name: "Алиса",
// email: "a@b.com",
// password: "secret" // Ошибка!
// };

Pick удобен для:

  • Создания публичных версий интерфейсов (без секретных полей)
  • Форм: нужна только часть полей
  • API: передаём только нужные данные
  • Пропсов компонентов: только нужные свойства
// Практический пример: пропсы для формы
interface FormData {
  name: string;
  email: string;
  password: string;
  confirmPassword: string;
}

// Для формы входа нужны только email и password
type LoginForm = Pick<FormData, "email" | "password">;

// Для формы регистрации нужны все поля
type RegisterForm = Pick<FormData, "name" | "email" | "password" | "confirmPassword">;

Omit<T, K> — исключение свойств

Omit<T, K> — противоположность Pick. Он создаёт тип, исключая указанные свойства:

// Оригинальный интерфейс
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

// Omit исключает id и createdAt
type CreateUser = Omit<User, "id" | "createdAt">;
// Эквивалентно:
// type CreateUser = {
// name: string;
// email: string;
// };

// Использование:
function createUser(data: CreateUser): User {
  return {
    ...data,
    id: Date.now(),
    createdAt: new Date()
  };
}

// Вызываем только с нужными полями:
createUser({ name: "Алиса", email: "a@b.com" }); // OK!

Omit удобен для:

  • Функций создания: исключаем id, createdAt, updatedAt
  • Функций обновления: исключаем неизменяемые поля
  • Пропсов компонентов: исключаем внутренние свойства

Комбинирование утилитарных типов

Утилитарные типы можно комбинировать друг с другом для создания сложных трансформаций:

// Оригинальный интерфейс
interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  createdAt: Date;
}

// 1. Исключаем id и createdAt
// 2. Делаем все свойства необязательными
type UpdateProduct = Partial<Omit<Product, "id" | "createdAt">>;
// Эквивалентно:
// type UpdateProduct = {
// name?: string;
// price?: number;
// description?: string;
// };

// Использование:
function updateProduct(id: number, updates: UpdateProduct): void {
  console.log(`Обновление продукта ${id}`, updates);
}

updateProduct(1, { price: 100 }); // OK!
updateProduct(1, { name: "Название", description: "Описание" }); // OK!

Практический пример: REST API

Покажем, как утилитарные типы помогают в типизации REST API:

// Полная модель данных
interface Article {
  id: number;
  title: string;
  content: string;
  authorId: number;
  createdAt: Date;
  updatedAt: Date;
}

// Типы для разных HTTP-методов:
// GET /articles — ответ
type ArticleResponse = Article;

// POST /articles — тело запроса (без id, createdAt, updatedAt)
type CreateArticle = Omit<Article, "id" | "createdAt" | "updatedAt">;

// PATCH /articles/:id — тело запроса (обновление, все поля необязательны)
type UpdateArticle = Partial<Omit<Article, "id" | "createdAt" | "updatedAt">>;

// GET /articles/:id — ответ
type ArticleDetail = Article;

// Публичный просмотр (без authorId)
type ArticlePublic = Pick<Article, "id" | "title" | "content">;

Практический пример: пропсы компонента

В React утилитарные типы часто используются для типизации пропсов:

// Базовые пропсы компонента
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary" | "danger";
  size?: "small" | "medium" | "large";
  disabled?: boolean;
}

// Disabled кнопка: onClick и disabled не нужны
type DisabledButtonProps = Omit<ButtonProps, "onClick" | "disabled"> & {
  disabled: true;
};

// Loading кнопка: нужен только label
type LoadingButtonProps = Pick<ButtonProps, "label" | "size"> & {
  loading: boolean;
};

Сводная таблица

Тип Описание Пример
Partial<T> Все свойства необязательны { name?: string; age?: number }
Required<T> Все свойства обязательны { name: string; age: number }
Pick<T, K> Только выбранные свойства Pick<User, 'name' | 'email'>
Omit<T, K> Все свойства кроме исключённых Omit<User, 'password'>

Итоги урока

  • Partial<T> — делает все свойства необязательными
  • Required<T> — делает все свойства обязательными
  • Pick<T, K> — выбирает только указанные свойства
  • Omit<T, K> — исключает указанные свойства
  • Утилитарные типы можно комбинировать
  • Они определены в стандартной библиотеке TypeScript
  • Используйте их для типизации API, пропсов, форм

Практический пример: типобезопасная валидация

Утилитарные типы помогают создавать типобезопасные функции валидации:

// Интерфейс с необязательными полями
interface RegistrationForm {
  name: string;
  email: string;
  password: string;
  confirmPassword: string;
  age: number;
  newsletter: boolean;
}

// Типы ошибок валидации
type ValidationErrors = Partial<Record<keyof RegistrationForm, string>>;

// Функция валидации
function validate(
  data: Partial<RegistrationForm>
): ValidationErrors {
  const errors: ValidationErrors = {};

  if (!data.name) errors.name = "Обязательное поле";
  if (!data.email) errors.email = "Обязательное поле";
  if (!data.password) errors.password = "Обязательное поле";

  return errors;
}

// Использование:
const errors = validate({ name: "Алиса" });
// errors.email = "Обязательное поле"
// errors.password = "Обязательное поле"

Практический пример: типизированный store

Создадим простой типизированный store с использованием Partial и Pick:

// Модель данных
interface Todo {
  id: number;
  title: string;
  completed: boolean;
  createdAt: Date;
}

// Тип для создания (без id и createdAt)
type CreateTodo = Omit<Todo, "id" | "createdAt">;

// Тип для обновления (все поля необязательны)
type UpdateTodo = Partial<Omit<Todo, "id" | "createdAt">>;

// Тип для фильтра (только title и completed)
type TodoFilter = Pick<Todo, "title" | "completed">;

// Store
class TodoStore {
  private todos: Todo[] = [];

  create(data: CreateTodo): Todo {
    const todo: Todo = {
      ...data,
      id: Date.now(),
      createdAt: new Date()
    };
    this.todos.push(todo);
    return todo;
  }

  update(id: number, data: UpdateTodo): void {
    // Обновляем только переданные поля
  }

  find(filter: TodoFilter): Todo[] {
    // Ищем по критериям
    return this.todos.filter(todo => {
      if (filter.title && !todo.title.includes(filter.title)) return false;
      if (filter.completed !== undefined && todo.completed !== filter.completed) return false;
      return true;
    });
  }
}

Практический пример: типобезопасные пропсы

Покажем, как утилитарные типы помогают в типизации React-компонентов:

// Базовые пропсы компонента
interface InputProps {
  label: string;
  value: string;
  onChange: (value: string) => void;
  placeholder?: string;
  disabled?: boolean;
  error?: string;
  type?: "text" | "password" | "email";
}

// Пропсы для числового ввода
type NumberInputProps = Omit<InputProps, "value" | "type"> & {
  value: number;
  onChange: (value: number) => void;
  min?: number;
  max?: number;
};

// Пропсы для textarea
type TextareaProps = Omit<InputProps, "type"> & {
  rows?: number;
  maxLength?: number;
};

Распространённые ошибки

Вот частые ошибки при использовании утилитарных типов:

Ошибка 1: Путаница между Pick и Omit
// Pick выбирает, Omit исключает
// Pick<User, "name" | "email"> — только name и email
// Omit<User, "password"> — всё кроме password
// НЕ путайте: Pick берёт, Omit убирает
Ошибка 2: Использование Partial вместо ?
// Partial делает ВСЕ свойства необязательными
// Если нужны необязательные поля — используйте ?
// interface User { name: string; age?: number; }
// Partial подходит для функций обновления
Ошибка 3: Слишком сложные комбинации
// Слишком сложно:
// Partial<Pick<Omit<User, "id">, "name" | "email">>
// Проще: создать отдельный интерфейс
// interface UserUpdate { name?: string; email?: string; }

Следующий урок

Утилитарные типы: Record, Readonly, Exclude, Extract — разберём оставшиеся встроенные типы

Тест по утилитарным типам

8 вопросов

Обновление пользователя

Premium